Context Map

개요

Bounded Context를 정의하는 것만으로는 부족하다. 전체를 보는 시야가 없으면 다음 문제가 남는다.

  • 다른 모델의 컨텍스트가 여전히 모호하고 계속 변한다
  • 다른 팀 사람들은 컨텍스트 경계를 잘 모르므로, 모르는 채로 경계를 흐리거나 연결을 복잡하게 만드는 변경을 한다
  • 경계가 분명해도, 다른 컨텍스트와의 관계가 모델의 성격이나 변경 속도에 제약을 건다. 이 제약은 주로 비기술적 경로로 나타나기 때문에 그것이 영향을 주는 설계 결정과 연결짓기 어렵다

Context Map은 이 관계들을 명시적으로 그리는 작업이다.


그리는 방법

Evans의 처방은 다음과 같다.

프로젝트에서 작동 중인 각 모델을 식별하고 그것의 Bounded Context를 정의하라. 여기에는 객체지향이 아닌 하위 시스템의 암묵적 모델도 포함된다. 각 Bounded Context에 이름을 붙이고, 그 이름을 보편 언어의 일부로 만들어라.

모델 사이의 접점을 기술하고, 모든 소통에 대한 명시적 번역을 개괄하며, 공유되는 것과 격리 장치, 그리고 영향력의 수준을 드러내라.

기존 지형을 먼저 그려라. 변형은 나중에 다룬다.

마지막 문장이 실무에서 중요하다. Context Map은 “이상적으로 이래야 한다"가 아니라 현재 실제로 어떤지를 먼저 그리는 도구다.


관계를 기술하는 기본 용어

용어정의
upstream-downstream상류 그룹의 행동은 하류 그룹의 프로젝트 성공에 영향을 주지만, 하류의 행동은 상류에 유의미한 영향을 주지 않는 관계. 같은 강가의 두 도시에서 상류 도시의 오염이 하류 도시에 영향을 주는 것과 같다. 상류 팀은 하류 팀의 운명과 무관하게 성공할 수 있다
mutually dependent별개 컨텍스트의 두 프로젝트가 둘 다 인도되어야 어느 쪽이든 성공으로 간주되는 상황
free다른 컨텍스트의 개발 방향·성패가 인도에 거의 영향을 주지 않는 컨텍스트

9가지 관계 패턴

1. Partnership

두 컨텍스트의 팀이 함께 성공하거나 함께 실패하는 경우 협력 관계가 형성된다.

상호 의존적인 하위 시스템의 조율이 부실하면 양쪽 프로젝트 모두 인도에 실패한다. 한 시스템에 핵심 기능이 빠지면 다른 시스템을 인도할 수 없게 되고, 상대의 기대에 맞지 않는 인터페이스는 통합을 실패시킨다.

처방: 두 컨텍스트 팀 사이에 파트너십을 맺고, 개발 계획을 조율하고 통합을 공동 관리하는 프로세스를 만든다. 상호 의존적인 기능은 같은 릴리스에 완료되도록 일정을 잡는다. 대부분의 경우 상대 하위 시스템의 모델을 상세히 이해할 필요는 없지만 프로젝트 계획은 반드시 조율해야 한다.

2. Shared Kernel

모델과 관련 코드의 일부를 공유하는 매우 밀접한 상호 의존이다. 설계 작업을 지렛대로 삼을 수도 있고 반대로 망가뜨릴 수도 있다.

처방: 팀들이 공유하기로 합의한 도메인 모델의 부분집합을 명시적 경계로 지정한다. 이 커널은 작게 유지한다. 이 경계 안에는 모델의 부분집합과 함께 그에 관련된 코드나 DB 설계의 부분집합을 포함시킨다. 이 명시적 공유물은 특별한 지위를 가지며, 상대 팀과 상의 없이 변경해서는 안 된다.

공통 모듈에 도메인 엔티티를 넣는 순간 모든 컨텍스트가 결합된다. 기술적 공통물(예외 기반 클래스, 시간 추상화, Money 같은 범용 Value Object)과 도메인 모델은 구분해야 한다. 전역 열거형 하나에 모든 에러 코드를 몰아넣는 것도 같은 실수다.

3. Customer/Supplier Development

상류 팀이 하류 팀의 운명과 무관하게 성공할 수 있는 upstream-downstream 관계다.

하류 팀은 상류의 우선순위에 휘둘려 무력해질 수 있고, 반대로 상류 팀은 하류를 깨뜨릴까 걱정해 위축될 수 있다. 번거로운 변경 요청 절차와 복잡한 승인 프로세스는 하류의 문제를 개선하지 못하고, 하류가 변경 거부권을 가지면 상류의 자유로운 개발이 멈춘다.

처방: 두 팀 사이에 명확한 고객/공급자 관계를 수립한다. 하류의 우선순위가 상류의 계획에 반영되도록 한다. 하류 요구사항에 대한 작업을 협상하고 예산을 배정해 모두가 그 약속과 일정을 이해하게 한다. 공동 개발한 자동화 인수 테스트를 상류 팀의 테스트 스위트에 추가하면, 상류 팀은 하류에 대한 부작용 걱정 없이 변경할 수 있다.

4. Conformist

upstream/downstream 관계이면서 상류가 하류의 요구를 들어줄 동기가 없는 경우, 하류 팀은 무력하다.

이타심에서 상류 개발자가 약속할 수는 있지만 지켜질 가능성은 낮다. 그 선의를 믿으면 하류 팀은 결코 제공되지 않을 기능에 기반해 계획을 세우게 된다.

처방: 상류 팀의 모델을 맹종적으로 따름으로써 컨텍스트 간 번역의 복잡도를 제거한다. 하류 설계자의 스타일을 제약하고 이상적인 모델이 나오지도 않지만, 순응을 택하면 통합이 엄청나게 단순해진다. 덤으로 상류 팀과 보편 언어를 공유하게 된다.

5. Anticorruption Layer (ACL)

잘 설계된 컨텍스트를 협조적인 팀들이 잇는 경우 번역 계층은 단순하고 우아할 수 있다. 그러나 통제나 소통이 Shared Kernel·Partnership·Customer/Supplier를 성립시킬 만큼 충분하지 않으면 번역은 훨씬 복잡해지고 방어적인 성격을 띠게 된다.

상류 시스템과의 큰 인터페이스는 결국 하류 모델의 의도를 통째로 압도하여, 하류 모델이 임시방편으로 상대 시스템의 모델을 닮아가게 만든다. 레거시 시스템의 모델은 대개 빈약하며, 잘 설계된 예외적인 경우조차 현재 프로젝트의 요구에 맞지 않아 상류 모델에 순응하는 것이 비현실적일 수 있다.

처방: 하류 클라이언트로서, 자기 도메인 모델의 용어로 상류 시스템의 기능을 제공하는 격리 계층을 만든다. 이 계층은 상대 시스템의 기존 인터페이스를 통해 대화하므로 상대 시스템은 거의 수정할 필요가 없다. 내부적으로 이 계층이 두 모델 사이를 필요한 방향으로 번역한다.

6. Open-host Service

일반적으로 각 Bounded Context는 외부 통합 대상마다 번역 계층을 하나씩 정의한다. 일회성 통합이라면 이 방식이 최소 비용으로 모델 오염을 막는다. 그러나 하위 시스템에 대한 수요가 많아지면 대상마다 번역기를 만드는 일이 팀을 짓누른다.

처방: 하위 시스템에 접근할 수 있는 프로토콜을 서비스 집합으로 정의하고 그것을 개방한다. 새로운 통합 요구를 처리하도록 프로토콜을 확장하되, 특정 팀만의 특이한 요구는 예외로 두고 일회성 번역기로 보완한다. 그래야 공유 프로토콜이 단순하고 일관되게 유지된다.

이 패턴은 서비스 제공자를 상류 위치에 놓는다. 각 클라이언트는 하류가 되며, 그중 일부는 Conformist가, 일부는 ACL을 만들게 된다.

7. Published Language

두 Bounded Context의 모델 사이를 번역하려면 공통 언어가 필요하다. 기존 도메인 모델을 직접 주고받는 것은 좋은 해법이 아니다. 그 모델은 지나치게 복잡하거나 잘못 분해되어 있을 수 있고, 대개 문서화되어 있지 않다. 무엇보다 데이터 교환 언어로 쓰이는 순간 사실상 동결되어 새로운 개발 요구에 대응할 수 없게 된다.

처방: 필요한 도메인 정보를 표현할 수 있는 잘 문서화된 공유 언어를 소통의 공통 매개로 사용하고, 필요에 따라 그 언어로/에서 번역한다. Open-host Service와 함께 쓰이는 경우가 많다.

8. Separate Ways

요구사항을 정의할 때는 냉정해야 한다. 두 기능 집합에 유의미한 관계가 없다면 완전히 떼어놓을 수 있다. 통합은 언제나 비싸고 때로는 그 이득이 작다.

처방: 한 Bounded Context가 다른 것들과 아무 연결도 갖지 않는다고 선언하여, 개발자들이 그 작은 범위 안에서 단순하고 특화된 해법을 찾게 한다.

9. Big Ball of Mud

기존 시스템을 조사하다 보면 모델이 뒤섞이고 경계가 일관되지 않은 부분, 종종 아주 큰 부분을 발견한다. 애초에 경계가 없는 시스템에서 컨텍스트 경계를 기술하려다 수렁에 빠지기 쉽다.

잘 정의된 컨텍스트 경계는 지적 선택과 사회적 힘의 결과로만 나타난다. 그런 요인이 없거나 사라지면 여러 개념 체계가 뒤섞이고 정의와 규칙이 모호하거나 모순되며, 기능이 추가될 때마다 임기응변 논리로 동작하게 된다. 의존이 소프트웨어를 가로지르고 인과관계 추적이 점점 어려워진다.

처방: 그 난장판 전체에 경계를 두르고 Big Ball of Mud로 지정한다. 이 컨텍스트 안에서 정교한 모델링을 시도하지 않는다. 다만 그런 시스템이 다른 컨텍스트로 번져나가는 경향을 경계한다.


선택 가이드

flowchart TD A[두 컨텍스트에 유의미한 관계가 있는가] -->|없다| SW[Separate Ways] A -->|있다| B[둘 다 인도되어야 성공인가] B -->|그렇다| P[Partnership] B -->|아니다| C[모델 일부를 공유할 통제력이 있는가] C -->|있다| SK[Shared Kernel] C -->|없다| D[상류가 하류 요구를 반영해 주는가] D -->|반영한다| CS[Customer/Supplier] D -->|반영하지 않는다| E[상류 모델을 그대로 써도 되는가] E -->|괜찮다| CF[Conformist] E -->|모델이 오염된다| ACL[Anticorruption Layer] F[통합 대상이 계속 늘어나는가] -->|그렇다| OHS[Open-host Service + Published Language]

실무에서의 적용

모놀리식 단계에서 컨텍스트 간 호출이 직접 메서드 호출로 이루어지더라도, 그 호출 지점 하나하나가 나중에 이벤트나 HTTP로 바뀔 자리다. 인터페이스를 미리 만들어 두기보다 호출 지점을 한 클래스에 모아두는 것이 실용적이다. 추상화는 변형이 두 번째로 나타났을 때 하면 된다.

Context Map은 이때 “어디를 모아둘 것인가"의 목록이 된다.


관련 문서


참고 자료

  • Eric Evans, Domain-Driven Design Reference (2015), IV. Context Mapping for Strategic Design
  • Vaughn Vernon, Implementing Domain-Driven Design (2013), Chapter 3 (Context Map 작성)
  • Martin Fowler, Bounded Context