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

[DDD Intro #4] Bounded Context: 모델은 경계 안에서만 일관된다

페이지 수정

DDD에서 가장 중요한 개념을 하나만 고르라면 많은 사람이 Bounded Context를 이야기할 것이다. 그런데 동시에 가장 많이 오해되는 개념이기도 하다.

Bounded Context를 처음 접하면 “서비스를 나누는 기준”이나 “패키지를 나누는 방법”으로 이해하기 쉽다. 물론 Bounded Context는 서비스 분리나 모듈 구조와 관련이 있다. 하지만 그 자체가 서비스나 패키지를 의미하는 것은 아니다.

Bounded Context는 모델의 경계다. 더 정확히 말하면, 하나의 모델과 하나의 언어가 일관되게 유지되는 경계다.

하나의 모델로 모든 것을 설명할 수 있을까

초기 시스템에서는 하나의 모델이 편해 보인다. 예를 들어 “회원”, “상품”, “주문”, “결제” 같은 개념을 공통 모델로 만들고 여러 기능에서 함께 사용한다. 중복이 줄어들고 재사용도 잘 되는 것처럼 보인다.

하지만 시스템이 커지면 같은 단어가 서로 다른 의미를 갖기 시작한다.

구독 서비스를 예로 들어보자. “구독”이라는 단어는 여러 팀에서 사용된다.

마케팅팀은 구독을 고객 세그먼트와 캠페인 대상으로 본다. 결제팀은 구독을 정기 결제 계약으로 본다. 콘텐츠 권한 팀은 구독을 사용자가 어떤 콘텐츠에 접근할 수 있는지 결정하는 기준으로 본다. 고객센터는 구독을 상담과 보상 처리의 대상이라고 본다.

모두 “구독”이라고 말하지만 관심사는 다르다.

마케팅 컨텍스트의 구독
- 무료 체험 여부
- 전환 가능성
- 캠페인 대상 여부

결제 컨텍스트의 구독
- 결제 주기
- 결제 수단
- 청구 실패 횟수
- 다음 결제 예정일

이용 권한 컨텍스트의 구독
- 접근 가능한 콘텐츠 범위
- 권한 시작일과 종료일
- 일시 제한 여부

고객센터 컨텍스트의 구독
- 상담 이력
- 보상 처리 가능 여부
- 직권 조정 사유

이 모든 것을 하나의 Subscription 모델로 합치면 어떻게 될까?

처음에는 공통 모델처럼 보이지만 시간이 지나면서 Subscription은 점점 커진다. 결제팀의 필드, 마케팅팀의 필드, 고객센터의 필드, 이용 권한 팀의 필드가 하나의 객체에 쌓인다. 어떤 변경이 어떤 기능에 영향을 주는지 알기 어려워진다. 한 팀의 언어가 다른 팀의 모델을 오염시킨다.

이때 필요한 것이 Bounded Context다.

모델은 경계 안에서만 일관된다

Bounded Context는 “이 경계 안에서는 이 단어가 이런 의미를 가진다”라고 선언하는 것이다.

결제 컨텍스트에서 Subscription은 정기 결제 계약을 의미할 수 있다. 이 모델은 결제 주기, 청구 금액, 결제 실패, 갱신 정책에 집중한다.

이용 권한 컨텍스트에서 Entitlement는 사용자가 현재 어떤 콘텐츠에 접근할 수 있는지를 의미할 수 있다. 이 모델은 결제 방식보다 접근 가능 기간과 권한 범위에 집중한다.

고객센터 컨텍스트에서 CustomerSubscription은 상담원이 고객에게 설명하고 조정할 수 있는 구독 상태를 의미할 수 있다. 이 모델은 보상, 예외 처리, 상담 이력에 집중한다.

같은 현실의 구독을 다루지만 모델은 다를 수 있다. 이것은 중복이 아니다. 오히려 서로 다른 문제를 하나의 모델로 억지로 합치지 않기 위한 분리다.

Bounded Context는 모델의 일관성을 지키는 울타리다.

공통 모델의 유혹

개발자는 중복을 싫어한다. 그래서 같은 단어가 보이면 공통 모델을 만들고 싶어진다.

public class Subscription {
    private SubscriptionId id;
    private MemberId memberId;
    private PlanId planId;
    private PaymentMethod paymentMethod;
    private LocalDateTime nextBillingAt;
    private LocalDateTime accessStartsAt;
    private LocalDateTime accessEndsAt;
    private boolean marketingTarget;
    private int supportCompensationCount;
    private SubscriptionStatus status;
    // ...
}

이 객체는 많은 정보를 담고 있다. 하지만 좋은 모델일까?

어떤 기능에서는 paymentMethod가 중요하지만, 어떤 기능에서는 필요 없다. 어떤 기능에서는 accessEndsAt이 중요하지만, 어떤 기능에서는 결제 실패 횟수가 더 중요하다. 어떤 기능에서는 supportCompensationCount가 핵심이지만, 대부분의 기능에서는 알 필요가 없다.

공통 모델은 중복을 줄이는 대신 의미를 흐리게 만들 수 있다. 특히 도메인 복잡성이 높은 시스템에서는 공통 모델이 점점 “모든 것을 아는 객체”가 된다. 결국 아무도 마음 편히 수정하지 못하는 모델이 된다.

중복을 제거하는 것보다 중요한 것은 의미를 지키는 것이다. Bounded Context는 이 균형을 잡게 해준다.

Bounded Context는 마이크로서비스가 아니다

Bounded Context를 이야기하면 자연스럽게 마이크로서비스를 떠올린다. 실제로 마이크로서비스를 설계할 때 Bounded Context는 매우 중요한 힌트를 준다. 하지만 둘은 같은 개념이 아니다.

Bounded Context는 모델의 경계다. Microservice는 배포의 경계다.

하나의 Bounded Context가 하나의 마이크로서비스가 될 수도 있다. 하지만 항상 그래야 하는 것은 아니다. 하나의 서비스 안에 여러 Bounded Context가 모듈로 존재할 수도 있고, 하나의 Bounded Context가 여러 배포 단위로 나뉠 수도 있다.

중요한 것은 서비스부터 나누지 않는 것이다.

주문 서비스
결제 서비스
배송 서비스
정산 서비스

이렇게 이름을 나누었다고 해서 자동으로 좋은 경계가 만들어지는 것은 아니다. 서비스는 나뉘었지만 모델은 여전히 서로 얽혀 있을 수 있다. 모든 서비스가 같은 Order DTO를 공유하고, 같은 상태 코드를 해석하고, 하나의 데이터베이스를 함께 바라본다면, 서비스는 나뉘었지만 모델의 경계는 흐릿하다.

먼저 물어야 할 질문은 이것이다.

이 단어는 어디까지 같은 의미를 갖는가?
이 모델은 어떤 문제를 해결하기 위해 존재하는가?
이 컨텍스트 안에서 지켜야 할 규칙은 무엇인가?
다른 컨텍스트와는 어떤 방식으로 협력해야 하는가?

마이크로서비스는 그다음의 선택이다.

경계는 조직과도 관련이 있다

Bounded Context는 순수하게 코드만 보고 결정할 수 없다. 모델은 사람들이 사용하는 언어와 연결되어 있고, 언어는 조직의 책임과 연결되어 있다.

결제팀이 쓰는 말과 고객센터가 쓰는 말이 다르다면, 그것은 단순한 커뮤니케이션 문제가 아닐 수 있다. 두 팀이 실제로 다른 문제를 풀고 있기 때문일 수 있다. 한쪽은 결제 성공과 실패를 중심으로 생각하고, 다른 한쪽은 고객 불만과 예외 보상을 중심으로 생각한다.

이런 경우 같은 모델을 강제로 공유하면 어느 한쪽의 언어가 다른 쪽에 맞춰 왜곡된다.

Bounded Context는 팀 구조와도 영향을 주고받는다. 같은 모델을 계속 함께 발전시켜야 한다면 긴밀한 협업이 필요하다. 반대로 서로 다른 속도로 변화하고, 서로 다른 정책을 다루고, 서로 다른 언어를 사용한다면 컨텍스트를 분리하는 것이 더 나을 수 있다.

좋은 경계는 기술적 의존성만 줄이는 것이 아니라, 팀이 각자의 도메인 언어를 지키며 독립적으로 발전할 수 있게 해준다.

Context 간 관계를 명시해야 한다

컨텍스트를 나누었다고 해서 서로 단절되는 것은 아니다. 현실의 시스템은 여러 컨텍스트가 협력해야 한다.

결제 컨텍스트에서 정기 결제가 성공하면 이용 권한 컨텍스트는 접근 권한을 갱신해야 한다. 고객센터 컨텍스트에서 보상으로 이용 기간을 연장하면 이용 권한 컨텍스트도 그 사실을 알아야 한다. 마케팅 컨텍스트는 무료 체험이 유료 구독으로 전환되었는지 알고 싶어 한다.

중요한 것은 컨텍스트 간 관계를 암묵적으로 두지 않는 것이다.

예를 들어 결제 컨텍스트의 내부 모델을 이용 권한 컨텍스트가 그대로 사용하면 어떻게 될까? 이용 권한 모델은 결제 모델에 종속된다. 결제팀의 모델 변경이 권한 시스템에 직접 영향을 준다.

이럴 때는 이벤트나 Published Language, Anti-Corruption Layer 같은 방식으로 관계를 명시할 수 있다.

Billing Context
  -> SubscriptionRenewed 이벤트 발행

Entitlement Context
  -> 이벤트를 구독하고 접근 권한 갱신

이 방식은 두 컨텍스트가 같은 모델을 공유하지 않도록 해준다. 결제 컨텍스트는 결제의 언어로 모델을 발전시키고, 이용 권한 컨텍스트는 권한의 언어로 모델을 발전시킬 수 있다.

경계를 찾는 신호

Bounded Context는 책상 앞에서 완벽하게 그릴 수 있는 것이 아니다. 시스템을 이해하고 대화하면서 점진적으로 발견된다. 다음과 같은 신호가 보이면 경계를 의심해볼 수 있다.

첫째, 같은 단어가 팀마다 다른 의미로 쓰인다.

“회원”, “고객”, “사용자”, “구독자”가 서로 혼용되고 있다면 각 단어가 어느 맥락에서 쓰이는지 살펴봐야 한다.

둘째, 하나의 모델이 너무 많은 책임을 가진다.

User, Order, Product, Subscription 같은 객체가 계속 커지고 있다면 여러 컨텍스트의 관심사가 섞였을 가능성이 있다.

셋째, 한 팀의 변경이 다른 팀의 기능을 자주 깨뜨린다.

이것은 코드 의존성 문제일 수도 있지만, 모델 경계가 흐릿하다는 신호일 수도 있다.

넷째, 조건문이 특정 업무 맥락을 계속 구분한다.

if (context == BILLING) { ... }
if (context == SUPPORT) { ... }
if (context == MARKETING) { ... }

이런 코드가 반복된다면 하나의 모델 안에 여러 컨텍스트가 억지로 들어와 있을 수 있다.

경계를 나누는 것은 비용이 든다

Bounded Context를 나눈다고 모든 문제가 해결되는 것은 아니다. 경계를 나누면 통합 비용이 생긴다. 데이터 중복도 생길 수 있고, 이벤트 전달이나 API 계약을 관리해야 한다. 트랜잭션을 하나로 묶기 어려워질 수도 있다.

따라서 무조건 잘게 나누는 것이 답은 아니다. 경계를 나누는 목적은 복잡성을 관리하는 것이다. 단순한 도메인을 지나치게 나누면 오히려 이해하기 어려운 시스템이 된다.

좋은 질문은 이것이다.

이 모델을 함께 유지하는 비용이 큰가?
분리했을 때 통합 비용보다 의미를 지키는 이점이 큰가?
두 영역은 서로 다른 속도로 변화하는가?
두 영역은 서로 다른 언어와 규칙을 갖는가?

Bounded Context는 복잡성을 줄이기 위한 도구이지, 모든 것을 잘게 쪼개기 위한 명분이 아니다.

마치며

모델은 경계 안에서만 일관된다. 하나의 단어가 항상 하나의 모델을 의미하지 않는다. 같은 “구독”이라도 결제 컨텍스트, 이용 권한 컨텍스트, 고객센터 컨텍스트에서 다른 모델이 될 수 있다.

Bounded Context는 이 차이를 인정하는 개념이다. 공통 모델 하나로 모든 문제를 해결하려는 유혹을 내려놓고, 각 맥락 안에서 가장 적절한 모델과 언어를 유지하게 해준다.

DDD에서 전략적 설계가 중요한 이유도 여기에 있다. 좋은 객체를 만드는 것보다 먼저, 모델이 유효한 경계를 찾아야 한다. 경계가 흐릿하면 아무리 Entity와 Value Object를 잘 만들어도 모델은 서로 오염된다.

다음 글에서는 이 경계 안에서 일관성을 지키는 전술적 설계의 핵심인 Aggregate를 다룬다. Aggregate는 관련 객체를 모아두는 단위가 아니라, 변경의 일관성을 보호하는 경계다.


페이지 수정
공유하기:

이전 글
[DDD Intro #5] Aggregate: 일관성의 경계
다음 글
[DDD Intro #3] 유비쿼터스 언어: 좋은 설계는 좋은 언어에서 시작된다