Software Architecture/DDD & Patterns
DDD Patterns - 07. 컨텍스트 통합
두 나라가 교역하는 법 — 바운디드 컨텍스트 통합
두 나라가 교역한다고 하자. 한 나라의 언어와 법이 다른 나라에 그대로 통하지 않는다. 공식 대사관(공개 API)을 두거나, 비공식 채널(이벤트 발행)로 소통한다. 국경을 넘을 때 세관(번역 계층)이 물건을 검수하고 규격을 맞춘다. 한 나라가 다른 나라의 법을 그대로 따르면(순응자) 주권을 잃고, 두 나라가 법을 공유하면(공유 커널) 갈등이 잦다.
바운디드 컨텍스트 간 통합도 같다. 주문 컨텍스트의 모델이 배송 컨텍스트에 그대로 통하지 않는다 — 두 컨텍스트는 서로 다른 언어(보편 언어)와 규칙을 쓴다. 통합은 두 나라의 외교처럼, 경계에서 번역하고 합의된 방식으로 소통하는 일이다.
동기 통합 — 대사관을 통한 공식 접촉
가장 단순한 통합은 동기 호출이다. 주문 컨텍스트가 결제 컨텍스트의 공개 API(OHS, 공개 호스트 서비스)를 호출한다. 한 나라가 대사관을 통해 다른 나라에 공식 요청을 하는 것과 같다.
flowchart LR
Order["주문 컨텍스트<br/>OrderService"] -->|"POST /payments<br/>(동기 API 호출)"| Payment["결제 컨텍스트<br/>Payment API (OHS)"]
동기 통합은 직관적이다 — 요청하고 응답을 받는다. 하지만 두 컨텍스트가 시간적으로 결합된다. 결제 서비스가 느려지면 주문도 느려지고, 결제가 죽으면 주문도 죽는다. 한 나라의 대사관이 문을 닫으면 외교가 끊기는 것과 같다.
비동기 통합 — 공식 발표로 소통
비동기 통합은 이벤트를 발행(P/L, 발행-구독)하는 방식이다. 주문 컨텍스트가 "주문 접수됨" 이벤트를 발행하면, 배송 컨텍스트가 그것을 구독해서 자기 일을 한다. 한 나라가 공식 발표를 하면, 다른 나라가 그것을 보고 반응하는 것과 같다.
flowchart LR
Order["주문 컨텍스트"] -.->|"OrderPlaced 이벤트 발행"| Bus[("메시지 큐")]
Bus -.->|"구독"| Shipping["배송 컨텍스트"]
Bus -.->|"구독"| Billing["청구 컨텍스트"]
비동기 통합에서 두 컨텍스트는 시간적으로 독립이다. 배송이 느려도 주문은 계속 접수된다 — 이벤트가 큐에 쌓이고 배송이 따라잡으면 처리한다. 대가는 최종 일관성이다 — 주문이 접수됐지만 배송 처리까지 시차가 생긴다(01-foundations의 두 아키텍처 비교에서 본 것과 같다).
방패막이 계층(ACL) — 세관과 통역사
한 컨텍스트가 다른 컨텍스트의 모델을 그대로 받아들이면, 외부 모델이 내부로 스며들어 내 모델을 오염시킨다. 결제 컨텍스트의 PaymentResponse 객체를 주문 컨텍스트가 그대로 쓰면, 결제가 응답 필드를 바꿀 때 주문 코드도 깨진다. 이걸 막는 것이 방패막이 계층(Anti-Corruption Layer, ACL)이다 — 세관이자 통역사다.
flowchart LR
OrderCtx["주문 컨텍스트"] -->|"PaymentCommand<br/>(주문 언어)"| ACL["ACL (번역 계층)"]
ACL -->|"POST /payments<br/>(결제 언어)"| PaymentCtx["결제 컨텍스트"]
PaymentCtx -->|"PaymentResponse<br/>(결제 언어)"| ACL
ACL -->|"PaymentResult<br/>(주문 언어)"| OrderCtx
ACL은 외부 모델을 내부 모델로 번역한다. 결제의 PaymentResponse(status, txn_id, avs_code)를 주문의 PaymentResult(success, transactionId)로 변환한다. 결제가 응답 형식을 바꿔도 ACL만 고치면 된다 — 주문 도메인은 모른다. 두 나라 사이에 통역사를 두는 것과 같다.
공유 커널 — 공동 통치 구역의 위험
두 컨텍스트가 일부 모델을 공유하는 것(Shared Kernel)은 양측이 함께 관리하는 공동 통치 구역이다. 편리하지만 위험하다 — 한쪽이 공유 모델을 바꾸면 양쪽 다 영향을 받는다. 두 나라가 같은 강을 공유하면, 한쪽이 오염시키면 양쪽이 피해를 본다. Evans는 공유 커널을 "필요한 경우에만, 그것도 최소 범위로" 쓰라고 경고한다.
통합 방식 선택 — 무엇이 맞나
| 기준 | 동기 (OHS) | 비동기 (P/L) |
|---|---|---|
| 응답을 즉시 필요 | 적합 | 부적합 |
| 결제 장애 시 주문 지속 | 부적합 | 적합 |
| 결합도 | 시간적 결합(느리면 같이 느려짐) | 느슨함(독립) |
| 일관성 | 강함 (요청-응답) | 최종 (이벤트 지연) |
선택 기준은 품질 속성이다 — 주문 접수 시 결제 결과를 즉시 알아야 하면 동기, 결제 장애에도 주문을 받아야 하면 비동기. 두 나라가 실시간 협상이 필요하면 대사관(동기), 소식을 전달하면 되면 공식 발표(비동기)다.
설계 사례 — ACL 구현과 이벤트 발행
두 컨텍스트가 통합할 때 번역 계층(ACL)이 어떻게 외부 모델을 내부 모델로 바꾸는지, 그리고 이벤트가 어떻게 발행되는지 코드로 본다.
ACL — 결제 응답을 주문 언어로 번역:
// 결제 컨텍스트의 응답 — 결제 도메인의 언어 (avs_code, processor_ref 등 주문은 모름)
public record PaymentResponse(String status, String txnId, String avsCode, String processorRef) {}
// 주문 컨텍스트의 모델 — 주문 도메인의 언어 (success, transactionId만 있음)
public record PaymentResult(boolean success, String transactionId) {}
// ACL (방패막이 계층) — 결제 언어를 주문 언어로 번역
public class PaymentAdapter {
private final PaymentGateway gateway; // 결제 컨텍스트의 공개 API
public PaymentResult charge(OrderId orderId, Money amount) {
PaymentResponse raw = gateway.charge(orderId.value(), amount.amount(), amount.currency());
return translate(raw); // 번역 — 결제의 세부 필드는 주문에 스며들지 않음
}
private PaymentResult translate(PaymentResponse raw) {
// 결제 status "CAPTURED" → 주문 success true
// 결제의 avsCode, processorRef는 버림 — 주문은 필요 없음
boolean success = "CAPTURED".equals(raw.status()) || "AUTHORIZED".equals(raw.status());
return new PaymentResult(success, raw.txnId());
}
}
결제가 응답 필드를 바꾸면(예: status → result_code) ACL의 translate 메서드만 고치면 된다 — 주문 도메인은 모른다. 통역사가 외국어 변화를 흡수하듯, ACL이 외부 모델 변화를 흡수한다.
이벤트 발행 — 주문 접수를 배송에 알림:
// 주문 컨텍스트 — 이벤트 발행 (비동기 통합)
public class OrderService {
private final OrderRepository repo;
private final EventPublisher publisher;
public void confirm(OrderId id) {
Order order = repo.findById(id).orElseThrow();
OrderPlaced event = order.confirm(); // 도메인에서 이벤트 생성
repo.save(order);
publisher.publish(event); // 메시지 큐로 발행 — 배송·청구가 구독
}
}
// 배송 컨텍스트 — 이벤트 구독 (자기 일만 함)
public class ShippingOnOrderPlaced {
public void on(OrderPlaced event) {
// 주문의 내부 구조(OrderLine 등)는 모름 — orderId만 받음
ShippingRequest req = ShippingRequest.forOrder(event.orderId(), event.customerId());
shippingService.schedule(req);
}
}
배송 컨텍스트는 OrderPlaced 이벤트에서 orderId와 customerId만 받는다 — 주문의 내부 구조(주문 항목, 총액, 상태)는 모른다. 두 컨텍스트가 주문 "언어"를 공유하지 않고, 이벤트(공식 발표)라는 합의된 통로로만 소통한다. 결제 장애나 배송 지연이 주문 컨텍스트에 전파되지 않는다 — 이벤트가 큐에서 대기하고, 각자 자기 속도로 처리한다.
참고
- Evans, Eric — (Addison-Wesley, 2003), Ch.14 (컨텍스트 통합 패턴) — 접근 2026-07-15
- Vernon, Vaughn — (Addison-Wesley, 2013), Ch.8·13 (ACL, 통합) — 접근 2026-07-15
- Fowler, Martin — "Anti-Corruption Layer" (Microservices 패턴), https://martinfowler.com/books/microservices.html — 접근 2026-07-15
'Software Architecture > DDD & Patterns' 카테고리의 다른 글
| DDD Patterns - 09. 리팩터링과 진화 (0) | 2026.07.21 |
|---|---|
| DDD Patterns - 08. 이벤트 스토밍 (0) | 2026.07.21 |
| DDD Patterns - 06. 헥사고날 실천 (0) | 2026.07.21 |
| DDD Patterns - 05. 빈약한 도메인 모델 (0) | 2026.07.21 |
| DDD Patterns - 04. 앤터프라이즈 패턴 (0) | 2026.07.21 |