DDD Bounded Context
도메인 모델이 일관된 의미를 유지하는 명시적 경계. 경계 안에서 Ubiquitous Language가 적용된다. 도메인 전문가와 개발자가 동일한 언어로 소통. 코드·문서·대화 모두 동일 용어 사용. 예: "Order"라는 단어가 영업 컨텍스트에서는 '고객 주문',...
Article
M
Me
hyunyoun's Blog
software-engineering-devopssoftware-engineeringdev-opsarchitecturedesignddd-bounded-contextoopddd10 min read
1. Overview
도메인 주도 설계와 바운디드 컨텍스트(DDD & Bounded Context)는 테이블 조인(Join)의 편의성 때문에 온 회사의 모든 로직이 하나의 거대한 DB에 엉켜버리는 모놀리스(Monolith)의 저주를 끊고, 비즈니스 언어(Ubiquitous Language)를 기준으로 시스템의 논리적/물리적 국경선을 긋는 아키텍처 분할술을 해부합니다.
학습자는 하나의 User 테이블에 영업팀, 배송팀, 결제팀이 각자의 컬럼 50개를 덕지덕지 붙여대는 '빅 볼 오브 머드(Big Ball of Mud)'의 재앙을 뜯어봅니다. 나아가 "배송팀의 User"와 "결제팀의 User"는 이름만 같을 뿐 완전히 다른 객체임을 인지하고, 양쪽을 쪼개어 독립적인 도메인 모델로 분리하는 **바운디드 컨텍스트(Bounded Context)**를 장악합니다. 마지막으로, 분리된 컨텍스트 간에 데이터를 동기화하기 위해 안티 코럽션 레이어(ACL)와 비동기 이벤트(Event-driven)를 활용하는 컨텍스트 매핑(Context Mapping) 역량을 확보합니다.
2. Scope & Boundaries
In-Scope
- Ubiquitous Language (보편적 언어): 기획자와 개발자가 코드 레벨까지 통일해서 쓰는 공통 언어 사전.
- Bounded Context (제한된 문맥): 언어의 의미가 유지되는 명확한 경계. (MSA의 논리적 단위).
- Context Mapping: 분리된 컨텍스트 간의 통신 방식 (ACL, Conformist, Open Host Service).
- Aggregates (애그리거트): 데이터 일관성이 유지되어야 하는 객체들의 묶음 (트랜잭션 단위).
Out-of-Scope
- Event Sourcing & CQRS: 상태를 이벤트로 저장하고 읽기/쓰기를 분리하는 구현 기술 별도의 고급 아키텍처 패턴이므로 분리.
- Entity & Value Object 구현체: 객체를 JPA(DB)로 매핑하는 기술적 디테일 09-04-02 ORM & Database Physics 영역으로 위임.
Boundaries
- DDD vs Data-Driven Design: 전통적 설계는 "DB 테이블을 어떻게 예쁘게(정규화) 짤까?"에서 출발합니다(데이터 중심). DDD는 "비즈니스 로직(도메인)이 무엇인가?"에서 출발하여 DB를 그저 데이터를 저장하는 부품(디테일)으로 격하시킵니다. 모델링의 주도권을 인프라(DB)에서 비즈니스(Domain)로 완벽히 가져오는 권력의 역전임을 명확히 긋습니다.
3. Counterexample
- 빅 볼 오브 머드 (Big Ball of Mud): 초기 스타트업이
Item이라는 테이블 하나를 만들었습니다. 시간이 지나며 재고팀, 마케팅팀, 정산팀이 각자의 요구사항을 반영하느라Item테이블의 컬럼이 150개가 되었습니다. 마케팅팀이 상품 노출 로직을 고쳤는데, 정산팀의 월말 결산 금액이 틀어지는 버그가 터집니다. 도메인 경계(Context)를 나누지 않고 하나의 모델에 모든 회사의 부서를 때려 박은 모놀리스의 파산입니다. - 가짜 마이크로서비스 (Distributed Monolith): 회사가 MSA를 하겠다며 결제 서버와 배송 서버를 물리적으로 쪼갰습니다. 그런데 결제 서버가 로직을 처리할 때 배송 서버의 DB 테이블을 직접 Select 해서 읽어옵니다. 배송 서버 개발자가 테이블 이름을 바꿨더니 결제 서버가 뻗습니다. 물리적으로는 쪼개졌으나 논리적(Bounded Context)으로는 완벽히 결합(Coupling)된 '분산 모놀리스'의 비극입니다.
4. Prerequisites
- 객체 지향 설계 (Basic): SOLID 원칙, 높은 응집도와 낮은 결합도. (09-03-08 SOLID)
- 마이크로서비스 아키텍처 (Basic): 모놀리스와 MSA의 차이. (07. System Architecture)
5. Learning Map
6. Learning Topics
Basic
Core Topic 01: 번역 비용의 소멸 (Ubiquitous Language)
- Why to Learn: 기획자는 "주문 취소"라고 부르고, 코드는
deleteOrder()로 되어 있으며, DB는status=9로 저장하는 등 하나의 행위를 3가지 언어로 번역하다가 버그가 터지는 바벨탑의 저주를 깨기 위함입니다. - What to Learn:
- Concepts: Ubiquitous Language (보편적 언어), Domain Expert (도메인 전문가), Model-Code Isomorphism (모델-코드 동형성).
- Skills: "사용자가 결제를 완료하면 VIP로 등급을 올린다"는 기획서의 문장을 그대로
user.completePayment(),user.upgradeToVIP()라는 1<1>1> 대응 코드로 모델링하기.
- How to Learn:
- 1단계: 바벨탑 현상: 도메인 전문가(영업팀)가 개발자에게 "이번에 A 정책이 변경됐어요"라고 하면, 개발자는 속으로 "아, DB 테이블의 저 컬럼을 이렇게 바꿔야겠군"이라며 기술적 언어로 자동 번역합니다. 이 번역 과정에서 도메인의 핵심 로직이 소실(Loss)됩니다.
- 2단계: 언어의 통일: DDD는 번역을 금지합니다. 전문가가 "주문이 '승인'되었다"라고 말하면, 코드에도
OrderStatus.APPROVED가 있어야 합니다. 코드만 읽어도 회사의 비즈니스가 어떻게 굴러가는지 소설책처럼 읽혀야 합니다. - 3단계: 행위(Behavior) 중심 모델링: 데이터(속성) 중심이 아니라 행동을 묘사합니다.
user.setGrade("VIP")같은 멍청한 Setter를 버리고,user.upgradeToVIP()라는 비즈니스 의미가 담긴 언어를 코드에 강제하는 렌더링을 해부합니다.
- Implement: Setter 척결과 Ubiquitous Language 매핑.
Before (Data-driven):order.setStatus("CANCEL"); order.setCancelDate(now);Action (Domain-driven): 도메인 전문가의 용어인 "주문 취소"라는 행위 자체를 객체에 부여.After:order.cancel();(메서드 내부에서 상태 변경과 시간 기록을 캡슐화). 기획자와 개발자가 코드를 보며 대화할 수 있는 경이로운 통일성 시각화.
Recommended
Core Topic 02: 분할과 정복 (Bounded Context)
- Why to Learn: 회사의 모든 로직을 껴안은 거대한
User,Item클래스(God Object)가 개발팀 전원의 발목을 잡는 재앙을 막고, 의미가 유지되는 최소 단위로 경계(Boundary)를 찢어내기 위함입니다. - What to Learn:
- Concepts: Bounded Context (제한된 문맥), Subdomain (Core, Supporting, Generic), MSA(마이크로서비스)와의 관계.
- Skills: 이커머스에서
상품(Item)이라는 단어가 '카탈로그 컨텍스트(전시용)'와 '재고 컨텍스트(물류용)'에서 완전히 다른 의미와 속성을 가짐을 증명하고, 물리적으로 분리하기.
- How to Learn:
- 1단계: 동음이의어의 비극: 하나의 거대한
Item클래스에 '이미지URL(전시팀)', '남은 재고량(재고팀)', '입점사 수수료율(정산팀)'이 다 붙어있습니다. 전시팀이 상품 이미지를 업데이트하려고Item객체를 로딩할 때, 전혀 필요 없는 수수료 로직까지 딸려와서 DB 락(Lock)을 걸어버립니다. - 2단계: 컨텍스트 분리: 언어의 의미가 통용되는 경계(Bounded Context)를 그립니다. "카탈로그 컨텍스트"에서의
Item은 '이름, 이미지, 가격'만 가집니다. "재고 컨텍스트"에서의Item은 '상품ID, 남은 수량, 창고 위치'만 가집니다. - 3단계: 물리적 분단 (MSA): 두
Item은 서로 다른 DB와 다른 서버로 분리됩니다. 카탈로그 팀은 하루에 10번씩 배포해도 재고 팀에 1비트의 영향도 주지 않는 완벽한 자치권(Autonomy)을 뜯어봅니다.
- 1단계: 동음이의어의 비극: 하나의 거대한
- Implement: 모놀리식 God Class Bounded Context 분할 모사.
Before: 50개의 컬럼을 가진tb_product테이블을 3개 팀이 공유.Action: 도메인 경계 분리.After:Catalog Context:Product(id, name, price, imageUrl)Inventory Context:StockItem(productId, quantity, location)Billing Context:SalesItem(productId, taxRate, sellerId)이름은 달라도 ID로 느슨하게 연결된 고도로 응집된 도메인 모델 분리 렌더링.
Practical
Core Topic 03: 국경 방어막 (Context Mapping & ACL)
- Why to Learn: 내 완벽하고 깨끗한 도메인(Context)이 외부의 썩은 레거시(Legacy) 시스템이나 다른 팀의 허접한 API 데이터를 받아먹다 같이 오염되는 끔찍한 사태를 방어하기 위함입니다.
- What to Learn:
- Concepts: Anti-Corruption Layer (ACL / 부패 방지 계층), Conformist (준수자), Open Host Service (OHS).
- Skills: 결제(외부) 시스템에서 내려주는 이상한 XML 포맷의 응답 값을 내 도메인의 깔끔한 객체로 변환해 주는 완충재(Adapter/ACL) 클래스 설계하기.
- How to Learn:
- 1단계: 외부의 침공: 우리 시스템(새로운 배송 시스템)은 외부 결제 PG사 API를 호출해 데이터를 받습니다. PG사가 응답으로
pay_dt_str: "20231010"같은 이상한 포맷을 던집니다. - 2단계: 오염 (Corruption): 개발자가 바쁘다며 저 이상한 변수명을 우리 시스템의 도메인 깊숙한 곳까지 그대로 가져다 씁니다. PG사가 나중에 API 스펙을 바꾸면 우리 시스템 100군데가 터집니다.
- 3단계: ACL (부패 방지 계층): 국경(경계)에 번역기(ACL)를 세웁니다. 밖에서 무슨 쓰레기 데이터가 오든, ACL 클래스가 이를 낚아채어 우리 도메인의 아름다운
LocalDateTime paymentDate객체로 번역(Mapping)해 넘겨줍니다. 외부의 변화로부터 내 핵심 도메인을 완벽히 격리하는 방어막을 해부합니다.
- 1단계: 외부의 침공: 우리 시스템(새로운 배송 시스템)은 외부 결제 PG사 API를 호출해 데이터를 받습니다. PG사가 응답으로
- Implement: ACL(Anti-Corruption Layer) 구현 코드 모사.
외부 데이터:LegacyERPResponse { item_cd: "A01", qty_str: "100" }ACL 번역기:class ErpTranslator { mapToDomain(response) { return new Inventory(id: response.item_cd, count: parse(response.qty_str)); } }내부 도메인: 오직 순수한Inventory객체만 취급함. 외부 ERP 시스템이 교체되어도 ACL 번역기만 수정하면 끝나는 방화벽 렌더링.
Advanced
Core Topic 04: 트랜잭션의 한계 (Aggregates)
- Why to Learn: "DB 테이블 10개를 한 번의 트랜잭션(Lock)으로 묶어서 갱신하자"는 모놀리스식 마인드로 마이크로서비스를 짰다가 서버 전체가 데드락(Deadlock)에 빠져 죽는 현상을 막기 위함입니다.
- What to Learn:
- Concepts: Aggregate (애그리거트), Aggregate Root (루트 엔티티), Eventual Consistency (최종 일관성), Invariants (불변 규칙).
- Skills: "주문(Order)"과 "주문 항목(OrderLine)"은 생명주기가 같으므로 하나의 애그리거트로 묶어 100% 강한 트랜잭션을 걸고, "결제(Payment)"는 다른 애그리거트이므로 이벤트를 발행해 비동기로 처리하기.
- How to Learn:
- 1단계: 경계 설정 (Aggregate): 연관된 객체들의 무리(Cluster)입니다. 1개의 애그리거트 = 1개의 트랜잭션 단위입니다.
주문(Order)과 그 밑에 딸린주문 상품(OrderItem)은 같이 생성되고 같이 삭제되므로 하나의 애그리거트로 묶습니다. - 2단계: 루트 (Aggregate Root): 외부에서는 오직 대장(Root)인
Order객체만 호출할 수 있습니다.OrderItem의 가격을 맘대로 바꾸는 건 금지됩니다. 무조건Order를 통해서만 내부 상태를 바꿀 수 있는 엄격한 데이터 일관성 룰을 강제합니다. - 3단계: 애그리거트 간의 통신:
주문(Order)이 완료되면회원(Member)의 포인트가 올라가야 합니다. 두 개는 서로 다른 애그리거트입니다. 절대 1개의 트랜잭션(DB 락)으로 묶지 마라! 주문이 완료되면 "주문 완료 이벤트(Event)"를 쏘고, 회원이 그걸 주워서 나중에(Eventually) 포인트를 올리는 분산 시스템의 역학을 뜯어봅니다.
- 1단계: 경계 설정 (Aggregate): 연관된 객체들의 무리(Cluster)입니다. 1개의 애그리거트 = 1개의 트랜잭션 단위입니다.
- Implement: Aggregate 간 비동기 이벤트 통신(Event-driven) 렌더링.
Before (동기/강결합):orderService.complete() { db.order.update(); db.member.updatePoint(); }(회원 DB 죽으면 주문도 실패).Action: 도메인 이벤트 발행 구조로 전환.After (비동기/느슨한 결합):order.complete() { publishEvent(new OrderCompleted(orderId)); }. 물리적 분산 환경에서도 도메인 객체가 자신의 책임(트랜잭션)만 깔끔하게 완수하고 빠지는 극한의 성능 최적화 시각화.
7. Terminology
8. References
Primary
- [P1] CS2023 - Software Engineering (SE) - Software Design (Domain-Driven Design concepts)
- [P5] SFIA - System Design (SYMD) - Data and Object Modeling
Secondary
- [Domain-Driven Design] Eric Evans - Tackling Complexity in the Heart of Software (Bounded Context, Ubiquitous Language)
- [Implementing Domain-Driven Design] Vaughn Vernon - Aggregates, Context Mapping
Industry
- [MartinFowler.com] - BoundedContext, AntiCorruptionLayer
- [Microsoft Architecture] - Design a DDD-oriented microservice
9. Final Checklist
Primary
- 개발자가
user.setStatus(3)같은 기술적이고 데이터 중심적인 코드를 짤 때, 이를 도메인 전문가의 보편적 언어(Ubiquitous Language)인user.suspendAccount()로 리팩터링하여 의도를 명확히 할 수 있는가? - 하나의 거대한
Item테이블에 전시, 재고, 정산 팀의 100개 컬럼이 떡칠된 빅 볼 오브 머드(Big Ball of Mud) 구조를, 바운디드 컨텍스트(Bounded Context)를 쪼개어 3개의 독립된 모델로 물리적/논리적으로 분할할 수 있는가?
Secondary
- 외부 결제 PG사 API의 이상한 XML 응답 포맷이 우리 팀의 핵심 결제 비즈니스 로직(도메인 모델)까지 침투하지 못하도록, 경계면에 부패 방지 계층(ACL) 어댑터를 설계하여 데이터를 순수 도메인 객체로 번역(Mapping)할 수 있는가?
-
주문(Order)과주문 상세 항목(OrderLine)을 하나의 애그리거트(Aggregate) 묶음으로 설계하고, 외부에서는 절대OrderLine을 직접 수정하지 못하게 막고 무조건 애그리거트 루트(Root)인Order객체를 거치도록 불변성(Invariants)을 통제할 수 있는가?
Industry
- 두 개의 서로 다른 애그리거트(예: 주문 애그리거트와 배송 애그리거트) 간의 데이터를 갱신할 때, 하나의 거대한 동기식 DB 트랜잭션으로 묶어 데드락(Deadlock)을 유발하는 대신 도메인 이벤트를 발행하여 최종 일관성(Eventual Consistency)으로 처리할 수 있는가?
- 데이터베이스 테이블 정규화에서 출발하는 전통적인 '데이터 주도 설계(Data-Driven Design)'의 한계를 논증하고, "도메인(비즈니스 로직)이 가장 중앙에 있고 DB는 플러그인일 뿐이다"라는 클린 아키텍처적 DDD의 권력 역전을 아키텍처 다이어그램으로 렌더링할 수 있는가?