본문으로 건너뛰기
Leo Jung
뒤로 가기

[DDD Intro #9] Context Map: 경계는 혼자 존재하지 않는다

페이지 수정

Bounded Context를 이해하면 모델을 하나로 합치려는 욕심을 내려놓게 된다. 같은 단어라도 맥락에 따라 다른 의미를 가질 수 있고, 하나의 거대한 모델보다 여러 개의 일관된 모델이 더 나을 수 있다는 사실을 받아들이게 된다.

하지만 Bounded Context를 나누는 것만으로 설계가 끝나지는 않는다. 현실의 시스템에서 컨텍스트는 혼자 존재하지 않는다. 구독 컨텍스트는 결제 컨텍스트와 연결되어야 하고, 권한 컨텍스트와도 연결되어야 하며, 알림 컨텍스트에 어떤 일이 일어났는지 알려야 한다.

문제는 연결이다. 경계를 나누지 않으면 모델이 뒤섞이고, 경계만 나누고 관계를 설계하지 않으면 시스템이 단절된다.

Context Map은 이 둘 사이의 균형을 잡기 위한 도구다.

시스템은 모델들의 관계로 이루어진다

구독 서비스를 예로 들어보자. 처음에는 Subscription 하나만 잘 모델링하면 될 것처럼 보인다. 하지만 실제 서비스가 커지면 여러 컨텍스트가 등장한다.

회원 컨텍스트
구독 컨텍스트
결제 컨텍스트
권한 컨텍스트
알림 컨텍스트
정산 컨텍스트

각 컨텍스트는 자신의 언어를 가진다.

회원 컨텍스트에서 중요한 것은 사용자의 가입 상태, 인증 정보, 약관 동의, 계정 잠금 여부일 수 있다. 구독 컨텍스트에서 중요한 것은 요금제, 갱신일, 만료일, 해지 가능 여부다. 결제 컨텍스트는 승인, 취소, 실패, 환불, 거래 식별자를 다룬다. 권한 컨텍스트는 사용자가 어떤 기능에 접근할 수 있는지를 판단한다.

이들은 서로 다른 문제를 해결한다. 따라서 모델도 달라야 한다. 하지만 완전히 독립적일 수는 없다.

결제가 성공하면 구독이 갱신되어야 한다. 구독이 만료되면 권한이 회수되어야 한다. 구독이 해지되면 알림이 발송되어야 한다. 정산 컨텍스트는 결제와 환불 정보를 바탕으로 매출을 계산해야 한다.

즉, Bounded Context는 독립적인 섬이 아니다. 각각의 경계를 지키면서도 서로 관계를 맺어야 한다.

Context Map은 관계를 드러낸다

Context Map은 컨텍스트 사이의 관계를 명시적으로 표현하는 방법이다. 단순히 박스와 화살표를 그리는 것이 목적은 아니다. 중요한 것은 관계의 성격을 이해하는 것이다.

예를 들어 구독 컨텍스트와 결제 컨텍스트가 있다고 하자. 구독 컨텍스트는 결제 결과를 알아야 한다. 하지만 구독 컨텍스트가 결제 시스템의 내부 모델을 그대로 가져오면 문제가 생긴다.

결제 컨텍스트는 이런 언어를 사용할 수 있다.

Transaction
Approval
Cancel
PartialCancel
MerchantUid
PgResponseCode

구독 컨텍스트는 이런 언어를 사용할 수 있다.

SubscriptionRenewed
RenewalFailed
GracePeriodStarted
SubscriptionExpired

두 컨텍스트는 같은 사건을 바라보더라도 다른 언어로 표현한다. Context Map은 이 차이를 무시하지 않는다. 오히려 “이 둘은 어떤 방식으로 연결되어야 하는가?”를 묻는다.

Customer/Supplier: 한쪽이 다른 쪽에 의존할 때

가장 흔한 관계 중 하나는 Customer/Supplier 관계다. 한 컨텍스트가 다른 컨텍스트의 결과를 필요로 하는 경우다.

구독 컨텍스트는 결제 컨텍스트의 결과를 필요로 한다. 이때 구독 컨텍스트는 고객이고, 결제 컨텍스트는 공급자라고 볼 수 있다. 구독 정책이 결제 결과에 따라 달라지기 때문이다.

이 관계에서 중요한 것은 요구사항 협의다. 결제 컨텍스트가 어떤 이벤트를 제공할지, 어떤 API를 공개할지, 실패 코드를 어떻게 표현할지에 따라 구독 컨텍스트의 모델이 영향을 받는다.

좋은 Customer/Supplier 관계에서는 공급자 컨텍스트가 고객 컨텍스트의 필요를 어느 정도 이해한다. 반대로 나쁜 관계에서는 공급자 컨텍스트가 자신의 내부 구조만 밀어붙이고, 고객 컨텍스트는 그 모델에 억지로 맞춘다.

Conformist: 어쩔 수 없이 따르는 관계

때로는 상대 모델을 바꿀 수 없다. 외부 PG사, 레거시 ERP, 다른 조직의 공통 플랫폼처럼 우리가 통제할 수 없는 시스템이 있다. 이때는 상대 모델을 그대로 따라야 할 수도 있다. 이것이 Conformist 관계다.

Conformist는 나쁘기만 한 선택은 아니다. 비용을 줄이는 현실적인 선택일 수 있다. 외부 시스템이 충분히 안정적이고, 우리의 도메인 복잡성을 크게 해치지 않는다면 그대로 따르는 것이 낫다.

하지만 Core Domain에서는 조심해야 한다. 핵심 도메인이 외부 모델에 맞춰 찌그러지면 장기적으로 큰 비용이 생긴다. 특히 외부 시스템의 용어가 우리 도메인의 언어를 대체하기 시작하면 위험하다.

subscription.approveTransaction(pgResponse);

이 코드가 구독 도메인 안에 들어오기 시작하면, 구독 모델은 결제 모델의 언어에 끌려가고 있는 것일 수 있다.

Shared Kernel: 일부 모델을 공유할 때

두 컨텍스트가 일부 모델을 공유하는 경우도 있다. 이를 Shared Kernel이라고 한다.

예를 들어 여러 컨텍스트가 공통으로 사용하는 Money, Currency, EmailAddress 같은 Value Object가 있을 수 있다. 또는 회원과 인증 컨텍스트가 계정 식별자에 대한 일부 규칙을 공유할 수도 있다.

Shared Kernel은 중복을 줄이고 일관성을 높일 수 있다. 하지만 공유는 항상 비용을 만든다. 공유된 모델을 바꾸려면 여러 컨텍스트가 함께 영향을 받는다. 따라서 Shared Kernel은 작고 안정적인 영역에만 제한하는 것이 좋다.

모든 것을 공통 모듈로 만들고 싶을 때는 의심해야 한다. 공통화는 설계를 단순하게 만들 수도 있지만, 서로 다른 모델을 억지로 묶는 결과가 될 수도 있다.

Separate Ways: 연결하지 않는 것도 설계다

모든 컨텍스트가 연결될 필요는 없다. 때로는 서로 별도로 가는 것이 가장 좋은 선택이다. 이것을 Separate Ways라고 볼 수 있다.

예를 들어 내부 운영용 통계 시스템이 구독 컨텍스트와 깊게 연결될 필요가 없다면, 배치로 데이터를 복사해서 독립적으로 분석할 수 있다. 실시간 일관성이 필요하지 않다면 굳이 복잡한 API 의존성을 만들 필요가 없다.

연결은 항상 비용이다. 데이터 동기화, 장애 전파, 버전 관리, 계약 관리가 따라온다. 따라서 “어떻게 연결할까?”만 묻지 말고 “정말 연결해야 하는가?”도 물어야 한다.

Anti-Corruption Layer로 이어지는 지점

Context Map을 그리다 보면 특히 위험한 관계가 보인다. 외부 시스템이나 레거시 시스템이 우리 Core Domain에 강하게 영향을 주는 경우다. 이때 필요한 것이 Anti-Corruption Layer다.

예를 들어 결제 PG사의 응답 모델을 구독 도메인에서 그대로 사용한다고 해보자.

if (pgResponse.getResultCode().equals("0000")) {
    subscription.setStatus(ACTIVE);
}

이 코드는 기술적으로는 동작할 수 있다. 하지만 구독 도메인이 PG사의 코드 체계에 직접 의존하게 된다. 시간이 지나면 PG사의 언어가 구독 도메인의 언어를 침식한다.

Context Map은 이런 위험을 발견하게 해준다. 어떤 컨텍스트와는 협력하면 되고, 어떤 컨텍스트와는 맞춰야 하며, 어떤 컨텍스트로부터는 내 모델을 보호해야 한다.

Context Map은 문서가 아니라 대화의 도구다

Context Map은 멋진 다이어그램을 만들기 위한 것이 아니다. 팀이 관계를 명확히 이해하기 위한 대화의 도구다.

다음과 같은 질문을 던지기 위해 사용한다.

이 컨텍스트들은 어떤 데이터를 주고받는가?
누가 누구의 모델에 의존하는가?
어떤 모델은 공유해도 안전한가?
어떤 관계는 ACL로 보호해야 하는가?
어떤 연결은 끊어도 되는가?
외부 시스템의 변경이 우리 Core Domain에 어떤 영향을 주는가?

이 질문에 답하지 않으면 컨텍스트 간 관계는 코드에 암묵적으로 숨어든다. API DTO가 도메인 객체가 되고, 외부 시스템의 상태 코드가 내부 정책이 되고, 공통 모듈은 모든 팀이 두려워하는 거대한 의존성이 된다.

마치며

Bounded Context는 모델의 경계를 만든다. Context Map은 그 경계들이 어떤 관계를 맺는지 보여준다.

경계를 나누는 것만으로는 충분하지 않다. 서로 다른 모델은 반드시 만난다. 중요한 것은 그 만남을 의식적으로 설계하는 것이다.

어떤 컨텍스트는 협력해야 한다. 어떤 컨텍스트는 따라야 한다. 어떤 컨텍스트와는 일부를 공유할 수 있다. 어떤 컨텍스트로부터는 내 모델을 보호해야 한다. 그리고 어떤 컨텍스트와는 굳이 연결하지 않는 것이 더 좋은 선택일 수 있다.

Context Map은 시스템을 코드 구조가 아니라 모델들의 관계로 바라보게 한다. 그 관점이 있어야 DDD는 단일 모델을 잘 만드는 것을 넘어, 큰 시스템 안에서 여러 모델이 함께 살아가는 방식이 된다.


페이지 수정
공유하기:

이전 글
[DDD Intro #10] Anti-Corruption Layer: 외부 모델로부터 내 모델을 지키기
다음 글
[DDD Intro #8] Core Domain: 모든 곳에 같은 정성을 쏟지 않는다