← all posts
DEV 2026.05.02 · 14 min read Intermediate

DDD는 어디서 경계를 긋는가

서브도메인 분류부터 Bounded Context, Ubiquitous Language, Context Map 패턴, ACL, 이벤트 스토밍, 마이크로서비스까지 — DDD의 전략적 설계가 공유하는 하나의 질문을 추적한다.


DDD의 전략적 설계는 7개 챕터에 걸쳐 서로 다른 개념을 다루는 것처럼 보인다. 서브도메인 분류, Bounded Context, Ubiquitous Language, Context Map, ACL, 이벤트 스토밍, 마이크로서비스. 그런데 이 모든 개념은 하나의 질문을 다른 각도에서 묻고 있다 — “어디서 경계를 긋는가?” 그 경계를 언제, 어떻게, 왜 긋는지에 대한 답이 DDD 전략 설계의 본질이다.

자원 배분이 먼저다

경계를 긋기 전에 먼저 물어야 할 것이 있다. “이 기능이 우리 비즈니스에서 얼마나 중요한가?” Core Domain은 경쟁사보다 잘해야 하는 영역이고, Supporting Domain은 Core를 지원하는 필수 기능이며, Generic Domain은 외부 솔루션으로 충분한 범용 기능이다.

이 분류가 중요한 이유는 자원 배분 때문이다. SMS 발송 시스템을 직접 구현하느라 시니어 개발자 3개월을 쓰는 동안, 핵심 추천 알고리즘에는 1개월밖에 투자하지 못하는 상황이 실제로 발생한다. SMS는 Generic이다 — AWS SNS에 2주면 연동된다. 추천 알고리즘이 Core다.

분류 기준

“이 기능을 잘 해야 경쟁에서 이기는가?” → Core. “없으면 Core가 동작하지 않는가?” → Supporting. “외부 솔루션으로 충분한가?” → Generic. 이 세 질문이 서브도메인 분류의 전부다.

자원의 불균등 배분이 전략이다. Core에 최고 개발자와 DDD를 집중하고, Generic은 연동 어댑터만 작성한다. 이 판단이 없으면 DDD를 어디에 적용해야 할지도 알 수 없다.

언어의 경계가 Context의 경계다

Core Domain이 어디인지 알았다면, 다음 질문은 “그 도메인의 경계를 어떻게 긋는가”다. Bounded Context는 하나의 Ubiquitous Language가 일관되게 적용되는 범위다.

“Order”라는 단어가 주문 팀과 배송 팀에서 다른 의미를 가진다면, 그것은 단순한 용어 혼선이 아니다. 두 Context가 아직 분리되지 않았다는 설계 신호다. 주문 Context의 Order는 구매 계약이고, 배송 Context의 Shipment는 물리적 패키지다. 이름이 달라야 개념이 명확해진다.

com.example/
  ├── order/domain/Order.java         ← 주문 계약
  ├── shipping/domain/Shipment.java   ← 배송 패키지
  └── payment/domain/Payment.java     ← 금융 거래

같은 사람을 주문 Context에서는 Customer, 배송 Context에서는 Recipient, CS Context에서는 Requester로 부른다. 억지로 통일하면 하나의 클래스에 60개 필드가 쌓인다. 언어 충돌을 발견할 때마다 Context 경계를 정제할 기회로 삼는다.

Context 경계가 너무 작으면 분산 모놀리스가 되고, 너무 크면 공유 모델의 문제가 재현된다. 적절한 크기는 팀 하나가 독립적으로 이해하고 배포할 수 있는 범위다.

팀 관계가 통합 패턴을 결정한다

두 Context를 연결하는 방법은 기술이 아니라 팀 관계가 결정한다. Context Map의 7가지 패턴은 이 관계의 유형을 명시한다.

대등하고 신뢰 관계인 두 팀이 공유하는 순수 Value Object는 Shared Kernel이다. 한 팀이 API를 제공하고 상대가 요구사항을 반영받을 수 있으면 Customer-Supplier다. 외부 시스템이 우리 요구를 들어줄 의지가 없고 모델도 이상하면 Anticorruption Layer가 필요하다.

ACL의 구조는 단순하다. 도메인이 Port 인터페이스를 정의하고, Translator가 외부 코드를 도메인 언어로 번역하며, Adapter가 이 둘을 연결한다. 물류사 API가 "DLV""DELIVERING"으로 바꿔도 LogisticsTranslator.translateStatus() 한 줄만 수정하면 된다. 도메인 코드는 무관하다.

private ShipmentStatus translateStatus(String code) {
    return switch (code) {
        case "DLV"  -> ShipmentStatus.IN_TRANSIT;
        case "DEL"  -> ShipmentStatus.DELIVERED;
        default -> throw new UnknownLogisticsStatusException(code);
    };
}

도메인 테스트에 외부 SDK가 필요해지는 순간이 ACL이 없다는 신호다.

도메인은 함께 발견한다

경계를 올바르게 긋기 위해서는 도메인을 먼저 이해해야 한다. 그런데 도메인은 개발자 혼자 기획서를 읽어서 이해할 수 없다. 이벤트 스토밍은 도메인 전문가와 개발자가 함께 도메인을 발견하는 워크숍이다.

주황색 포스트잇에 과거형 이벤트를 붙인다 — [예약이 접수됨], [수납이 완료됨], [수술이 예약됨]. 언어가 바뀌는 지점이 Context 경계의 후보다. 워크숍 4시간 만에 “수술 예약”과 “진료 예약”이 완전히 다른 프로세스라는 사실을 발견하는 것이 가능하다. 이것을 3개월 구현 후에 발견하는 비용과 비교하면 이벤트 스토밍의 가치가 명확해진다.

이벤트 스토밍의 결과물이 곧 DDD 코드의 뼈대가 된다. Domain Event → 이벤트 클래스, Command → Application Service 메서드, Aggregate → Aggregate Root 클래스, Policy → Domain Event Handler, Context → 패키지 구조.

Context가 서비스보다 먼저다

마지막 질문 — “이 Context를 언제 마이크로서비스로 분리해야 하는가?” 많은 팀이 이 순서를 뒤집어서 시작한다. Context 없이 서비스를 먼저 분리하면 분산 모놀리스가 된다. 서비스 간 동기 HTTP 호출 체인, 장애 전파, 여러 서비스를 동시에 배포해야 하는 상황 — 물리적으로 분리됐지만 결합은 모놀리스보다 더 강하다.

올바른 순서는 반대다. 패키지로 논리적 Context 경계를 먼저 안정화하고, 이벤트 기반 통신으로 교체하고, 가장 독립적인 Context부터 물리적으로 분리한다.

트레이드오프

모놀리스 + DDD(패키지 경계)는 Context가 안정되면 언제든 분리 가능하다. 반면 Context 없이 분리한 서비스는 재조정 비용이 분리 비용보다 크다. Martin Fowler의 “Monolith First”가 이 순서를 지지한다.

DDD는 마이크로서비스를 위한 것이 아니다. 모놀리스 안에서도 Context 경계, Aggregate, Domain Event, Ubiquitous Language의 가치는 동일하다. 마이크로서비스는 DDD의 결과물이지 시작점이 아니다.

정리

  • 서브도메인 분류가 자원 배분의 근거다. Core를 모르면 DDD를 어디에 적용해야 할지 모른다.
  • 언어 충돌이 Context 경계 오류의 신호다. 충돌을 발견하면 경계를 정제하는 기회다.
  • 팀 관계가 통합 패턴을 결정한다. 기술을 선택하기 전에 관계를 먼저 파악한다.
  • 이벤트 스토밍은 도메인 전문가와 함께 발견하는 과정이다. 구현 전에 오해를 찾는다.
  • Context가 서비스보다 먼저다. 경계가 안정되지 않은 서비스 분리는 분산 모놀리스다.

이 다섯 가지는 서로 다른 개념이 아니다. “어디서 경계를 긋는가”라는 하나의 질문을 전략, 언어, 관계, 발견, 배포의 다섯 각도에서 바라본 것이다.