1. BC 간 통합 패턴 전체 목록
Bounded Context 간 통합 방식을 결정하는 것이 컨텍스트 매핑(Context Mapping)입니다.
IDDD(Implementing Domain-Driven Design)에서는 다음과 같은 패턴들을 제시합니다.
각 패턴은 두 BC 사이의 팀 관계와 기술적 통합 방식을 함께 표현합니다.
| 패턴 | 핵심 | Upstream/Downstream |
| 파트너십 (Partnership) | 두 팀이 대등하게 협력, 함께 일정 조율 | 없음 (대등) |
| 공유 커널 (Shared Kernel) | 일부 도메인 모델을 두 BC가 공유 | 없음 (공유) |
| 고객-공급자 (Customer-Supplier) | Upstream이 Downstream 요구사항을 수용 | U/D 있음 |
| 순응주의자 (Conformist) | Downstream이 Upstream 모델을 그냥 따름 | U/D 있음 |
| OHS/PL | Upstream이 공개 API + 공용 언어로 노출 | U/D 있음 |
| ACL (Anti-Corruption Layer) | Downstream이 번역 레이어로 오염 방지 | U/D 있음 |
| 분리된 방법 (Separate Ways) | 통합 포기, 각자 독립 구현 | 없음 |
| 큰 진흙공 (Big Ball of Mud) | 레거시 혼돈 시스템, ACL로 격리 | - |
OHS/PL과 ACL은 나머지 패턴들과 추상화 수준이 다릅니다.
고객-공급자, 순응주의자 등은 팀 관계를 기술하고,
OHS/PL과 ACL은 기술적 통합 방식을 기술합니다. 따라서 함께 조합해서 사용합니다.
분리된 방법(Separate Ways) 은 두 BC 간에 아예 통합하지 않기로 결정하는 패턴입니다.
언뜻 나쁜 설계처럼 보이지만, 핵심은 "통합 안 한다"는 걸 명시적으로 결정하는 것입니다.
무의식적으로 방치하는 것과, 컨텍스트 맵에 Separate Ways로 표기하는 것은 완전히 다릅니다.
2. 고객-공급자 vs 순응주의자
| 고객-공급자 | 순응주의자 | |
| Downstream의 힘 | 있음 (요구사항 협상 가능) | 없음 |
| Upstream 팀 관계 | 협력적 | 일방적 |
| 도메인 순수성 | 지킬 수 있음 | 오염 위험 |
| 선택 가능 조건 | 같은 조직, 협력 문화 | - |
이상적으로는 고객-공급자가 낫지만, 순응주의자가 강제되는 상황도 있습니다.
- Upstream이 외부 서비스라 협상 자체가 불가한 경우 (결제 PG사, 공공 API 등)
- Upstream 팀이 다른 조직이라 요구사항을 전달할 채널이 없는 경우
- Upstream이 레거시 시스템이라 변경 비용이 너무 큰 경우
이 경우엔 순응주의자를 택하되, ACL을 반드시 붙여서 외부 모델이 내 도메인으로 스며드는 걸 막는 것이 현실적인 전략입니다.
3. OHS / PL 상세
OHS (Open Host Service)
Upstream BC가 자신의 기능을 공개된 서비스 형태로 제공하는 패턴입니다.
핵심은 특정 Downstream만을 위한 전용 API가 아니라, 누구나 사용할 수 있는 범용 인터페이스를 제공한다는 점입니다.
// 특정 팀만을 위한 전용 API (OHS가 아님)
@GetMapping("/orders/for-inventory-team")
public InventoryTeamSpecificResponse getForInventory(...) { ... }
// OHS: 범용적으로 설계된 공개 인터페이스
@GetMapping("/orders/{orderId}")
public OrderResponse getOrder(@PathVariable String orderId) { ... }
| OHS | 전용 API | |
| 대상 | 불특정 다수 Downstream | 특정 팀 |
| 변경 시 | 하위 호환성 유지 필요 | 해당 팀과 협의 |
| 설계 기준 | 범용성 | 요청자 맞춤 |
PL (Published Language)
OHS가 "어떻게 노출할지"라면, PL은 "어떤 언어(포맷/스펙)로 노출할지" 입니다.
OHS와 항상 함께 다닙니다.
OHS → "REST API로 공개한다"
PL → "이 OpenAPI 스펙 문서가 계약이다"
PL의 실제 형태는 OpenAPI 스펙, Protobuf, Avro 스키마 등이 있습니다.
# OpenAPI 스펙 (PL의 전형적인 예)
components:
schemas:
OrderResponse:
properties:
orderId:
type: string
status:
type: string
enum: [PENDING, CONFIRMED, CANCELLED] # 이 용어 정의 자체가 PL
PL이 중요한 이유는 용어의 표준화 때문입니다.
status가 어떤 값을 가질 수 있는지, orderId가 어떤 형식인지를 문서화된 계약으로 명시해야
Downstream이 안정적으로 사용할 수 있습니다.
4. ACL (Anti-Corruption Layer) 상세
왜 필요한가
ACL 없이 Upstream의 모델을 그대로 사용하면 외부 용어가 내 도메인까지 침투합니다.
// ACL 없이 외부 모델이 도메인까지 침투한 상태
public class Order {
private String externalInventoryItemCode; // 외부 용어가 내 도메인에
private int remainingCount; // 외부 용어가 내 도메인에
}
외부 시스템이 itemCode를 productCode로 바꾸는 순간, 내 도메인 모델까지 수정해야 합니다.
ACL은 이 의존성 전파를 차단합니다.
ACL의 세 가지 책임
1. 모델 번역
외부 필드명/타입을 내 도메인 모델의 언어로 바꿉니다.
private Stock translate(ExternalInventoryResponse response) {
// 외부 용어(remainingCount) → 내 도메인 모델(Stock)
return new Stock(response.getRemainingCount());
}
2. 개념 매핑
외부 시스템과 내 도메인의 개념 자체가 다를 때 더 중요해집니다.
단순한 필드 변환이 아니라, 의미의 재해석이 필요합니다.
private OrderStatus translateStatus(ExternalOrderStatus externalStatus) {
// 외부 시스템: 숫자 코드로 상태 표현
// 내 도메인: 명확한 의미를 가진 열거형으로 표현
return switch (externalStatus.getCode()) {
case 1 -> OrderStatus.PENDING;
case 2 -> OrderStatus.CONFIRMED;
case 3, 4 -> OrderStatus.CANCELLED; // 외부의 2가지 상태 → 내 도메인의 1가지로 통합
default -> throw new UnknownStatusException(externalStatus.getCode());
};
}
3. 외부 모델 캡슐화
외부 응답 객체(ExternalInventoryResponse)가 Adapter 밖으로 새어나가지 않도록 막습니다.
@Component
public class ExternalInventoryAdapter implements InventoryPort {
@Override
public Stock checkStock(ProductId productId) {
ExternalInventoryResponse response = externalClient.getInventory(...);
return translate(response);
// ExternalInventoryResponse는 이 클래스 밖으로 절대 나가지 않음
}
}
ExternalInventoryResponse가 Adapter 밖으로 나가는 순간 ACL이 깨집니다.
Application이나 Domain이 외부 모델 타입을 직접 참조하게 되면, 외부 변경이 내부로 전파됩니다.
ACL을 별도 클래스로 분리할 때
번역 로직이 복잡해지면 ACL을 별도 클래스로 명시적으로 분리하기도 합니다.
// 번역 책임만 담당하는 ACL 클래스
@Component
public class InventoryAcl {
public Stock toStock(ExternalInventoryResponse response) {
return new Stock(response.getRemainingCount());
}
public ProductId toProductId(String externalItemCode) {
// 외부 코드 형식 → 내 도메인 ID 형식으로 변환
return new ProductId(externalItemCode.replace("EXT-", ""));
}
}
// Adapter는 외부 호출만 담당하고, 번역은 ACL에 위임
@Component
public class ExternalInventoryAdapter implements InventoryPort {
private final ExternalInventoryClient externalClient;
private final InventoryAcl inventoryAcl;
@Override
public Stock checkStock(ProductId productId) {
ExternalInventoryResponse response = externalClient.getInventory(...);
return inventoryAcl.toStock(response);
}
}
번역 로직이 단순하면 Adapter 안의 private 메서드로 두고,
복잡해지면 별도 클래스로 분리하는 것이 적절한 기준입니다.
5. 헥사고날 아키텍처와의 관계
OHS/ACL은 DDD 전략 패턴이고, 포트/어댑터는 전술적 구현 수단입니다.
두 개념은 추상화 수준이 다르며, OHS/ACL을 구현하는 데 헥사고날 아키텍처를 사용하는 관계입니다.

| DDD 전략 패턴 | 헥사고날 대응 개념 | 방향 |
| OHS | Incoming Adapter + Incoming Port | 외부 → 내 BC |
| ACL | Outgoing Adapter 내부 | 내 BC → 외부 |
전체 흐름 코드
Incoming Adapter (OHS의 구현체)
@RestController
@RequestMapping("/orders")
public class OrderController {
private final CreateOrderUseCase createOrderUseCase;
@PostMapping
public ResponseEntity<CreateOrderResponse> createOrder(
@RequestBody CreateOrderRequest request) {
CreateOrderCommand command = new CreateOrderCommand(
new CustomerId(request.getCustomerId()),
new ProductId(request.getProductId()),
request.getQuantity()
);
OrderId orderId = createOrderUseCase.createOrder(command);
return ResponseEntity.ok(new CreateOrderResponse(orderId.getValue()));
}
}
Incoming Port
public interface CreateOrderUseCase {
OrderId createOrder(CreateOrderCommand command);
}
Application Service
@Service
@Transactional
public class CreateOrderService implements CreateOrderUseCase {
private final OrderRepository orderRepository;
private final InventoryPort inventoryPort;
@Override
public OrderId createOrder(CreateOrderCommand command) {
// Outgoing Port를 통해 재고 확인 — 외부 시스템이 어떻게 생겼는지 모름
Stock stock = inventoryPort.checkStock(command.getProductId());
if (!stock.isAvailable(command.getQuantity())) {
throw new InsufficientStockException();
}
Order order = Order.create(
command.getCustomerId(),
command.getProductId(),
command.getQuantity()
);
orderRepository.save(order);
return order.getId();
}
}
Outgoing Port (내 도메인 언어로만 정의)
// 외부 재고 시스템이 어떻게 생겼는지 전혀 모른다
// 오직 내 도메인 언어(Stock, ProductId)만 사용
public interface InventoryPort {
Stock checkStock(ProductId productId);
}
Outgoing Adapter (ACL의 실체)
@Component
public class ExternalInventoryAdapter implements InventoryPort {
private final ExternalInventoryClient externalClient;
@Override
public Stock checkStock(ProductId productId) {
ExternalInventoryResponse response =
externalClient.getInventory(productId.getValue());
return translate(response); // ACL의 핵심: 외부 모델 → 내 도메인 모델
}
private Stock translate(ExternalInventoryResponse response) {
return new Stock(response.getRemainingCount());
}
}
6. 고객-공급자와 OHS/ACL의 공존
세 패턴은 서로 다른 관점을 다루기 때문에 함께 사용할 수 있습니다.
| 패턴 | 관점 | 역할 |
| 고객-공급자 | 조직 | 두 팀이 어떻게 협력할 것인가 |
| OHS/PL | 인터페이스 | Upstream이 무엇을 어떻게 노출할 것인가 |
| ACL | 번역 | Downstream이 어떻게 자신을 보호할 것인가 |

- 고객-공급자: 주문 팀이 요구사항을 전달하고, 재고 팀이 수용해서 OHS를 설계하는 팀 간 협력 방식
- OHS/PL: 재고 팀이 실제로 REST API와 OpenAPI 스펙을 만들어 공개하는 방식
- ACL: 주문 팀이 재고 API의 모델이 자신의 도메인을 오염시키지 않도록 번역하는 방식
세 패턴은 같은 통합을 각각 다른 레이어에서 바라보기 때문에 충돌 없이 함께 사용됩니다.
7. 정리
OHS/PL과 ACL은 통합의 양쪽 끝을 담당합니다.

- OHS/PL: Upstream이 노출을 표준화
- ACL: Downstream이 수신을 보호
두 패턴은 서로를 전제하는 관계입니다.
OHS/PL이 잘 정의되어 있을수록 ACL의 번역 로직이 단순해지고,
ACL이 잘 구현되어 있을수록 Upstream의 변경이 내 도메인에 미치는 영향이 줄어듭니다.
'Architecture > DDD' 카테고리의 다른 글
| [DDD] 외부 Context 통합의 비가용성, DDD와 헥사고날 아키텍처로 다루기 (0) | 2026.06.24 |
|---|---|
| [DDD] 검증은 어디서 해야할까? DTO, Command, Domain의 책임 분리 (0) | 2026.05.12 |