[도메인주도개발] Ch9 : 도메인 모델과 바운디드 컨텍스트
9.1 도메인 모델과 경계
모델은 특정한 컨텍스트(문맥) 하에서 완전한 의미를 갖는다. 각 모델은 경계를 가지도록 해서 섞이지 않도록 해야 하는데, 여러 하위 도메인 모델이 섞이기 시작하면 각 하위 도메인별로 다르게 발전하는 요구사항을 모델에 반영하기 어려워진다. 이렇게 구분되는 경계를 갖는 컨텍스트를 DDD 에서는 바운디드 컨텍스트(Bounded Context) 라고 한다.
처음 도메인 모델을 만들 때 주의해야 하는 것은 한 개의 모델로 여러 하위 도메인을 모두 표현하려고 하면 안된다는 것이다. 이렇게 만들 경우 모든 하위 도메인에 맞지 않는 모델을 만들게 될 수 있다. 또 논리적으로 같은 존재처럼 보이지만 하위 도메인에 따라 다른 용어를 사용하는 경우도 있다. 따라서 올바른 도메인 모델을 설계하기 위해서는 하위 도메인마다 모델을 만들어야 한다.
예를 들어 상품이라는 모델이 있을 수 있다. 주문에서의 상품, 재고 관리에서의 상품, 카탈로그에서의 상품은 이름은 같지만 실제로는 의미하는 것이 다르다.
또 시스템을 사용하는 사람을 회원 도메인에서는 회원이라고 부르지만, 주문 도메인에서는 주문자라고 부르고, 배송 도메인에서는 보내는 사람이라고 부르기도 한다.
9.2 바운디드 컨텍스트
바운디드 컨텍스트는 모델의 경계를 결정하며 한 개의 바운디드 컨텍스트는 논리적으로 한 개의 모델을 갖는다.
바운디드 컨텍스트는 용어를 기준으로 구분한다.
카탈로그 컨텍스트와 재고 컨텍스트는 서로 다른 용어를 사요하므로 이 용어를 기준으로 컨텍스트 분리가 가능하다.
도메인 모델은 이 바운디드 컨텍스트 안에서 도메인을 구현한다.
이상적으로는 하위 도메인과 바운디드 컨텍스트가 일대일 관계를 가지면 좋겠지만, 실제로는 조직 구조에 따라 바운디드 컨텍스트가 결정된다. 또는 용어를 명확하게 구분하지 못해서 두 하위 도메인을 하나의 바운디드 컨텍스트에서 구현하기도 한다. 규모가 작은 기업은 전체 시스템을 한 개 팀에서 구현할 때도 있는데, 이 경우 여러 하위 도메인을 한 개의 바운디드 컨텍스트에서 구현하기도 한다.
주문 하위 도메인에서 주문을 처리하는 팀과 복잡한 금액 계산 로직을 구현하는 팀이 따로 있을 수 있다. 이 경우 주문 하위 도메인에 주문 바운디드 컨텍스트와 결제 금액 바운디드 컨텍스트가 존재하게 된다.
여러 하위 도메인을 하나의 바운디드 컨텍스트에서 개발할 때에는 하위 도메인의 모델이 섞이지 않도록 주의해야한다. 이를 위해 한 개의 바운디드 컨텍스트가 여러 하위 도메인을 포함하더라도 하위 도메인마다 구분되는 패키지를 갖도록 구현해야 한다. 이를 통해 각 하위 도메인마다 바운디드 컨텍스트를 갖는 효과를 낼 수 있다.
만약 하위 도메인간의 경계가 모호해질 경우, 다음의 상황이 야기될 수 있다.
- 하위 도메인별로 기능을 확장하기 어려워진다. (도메인 모델이 개별 하위 도메인을 제대로 반영하지 못함.)
- 서비스 경쟁력 저하
또 바운디드 컨텍스트는 도메인 모델을 구분하는 경계가 되기 때문에 바운디드 컨텍스트는 구현하는 하위 도메인에 알맞은 모델을 포함한다.
9.3 바운디드 컨텍스트 구현
바운디드 컨텍스트는 도메인 기능을 제공하는 데 필요한 모든 요소(표현 영역, 응용 서비스, 인프라스트럭처 영역)을 모두 포함하며, 각 바운디드 컨텍스트는 도메인에 알맞은 아키텍처를 사용하여 구현한다. 또 바운디드 컨텍스트는 UI를 가지지 않을 수도 있고, 또는 UI 서버를 통해 간접적으로 브라우저와 통신할 수도 있다.
바운디드 컨텍스트는 도메인 모델만 포함하는 것은 아니고, 도메인 기능을 제공하는 데 필요한 모든 요소를 포함한다.
모든 바운디드 컨텍스트를 반드시 도메인 주도로 개발할 필요는 없고, 각 바운디드 컨텍스트는 도메인에 알맞은 아키텍처를 사용하여 구현한다. 서비스-DAO 구조를 사용하면 도메인 기능이 서비스에 흩어지게 되지만 도메인 기능 자체가 단순하면 서비스-DAO 로 구성된 CRUD 방식을 사용해도 코드를 유지 보수하는 데에 문제가 되지 않는다.
한 바운디드 컨텍스트에서 두 방식을 혼합해서 사용할 수도 있는데, 대표적인 예가 CQRS 패턴이다.
CQRS(Command Query Responsibility Segregation
: 상태를 변경하는 명령 기능과 내용을 조회하는 쿼리 기능을 위한 모델을 구분하는 패턴.
이 패턴을 단일 바운디드 컨텍스트에 적용하면 상태 변경과 관련된 기능은 도메인 모델 기반으로 구현하고 조회 기능은 서비스-DAO를 이용해서 구현할 수 있다.
각 바운디드 컨텍스트는 서로 다른 구현 기술을 사용할 수도 있다.
웹 MVC는 스프링 MVC를 사용하고 리포지터리 구현 기술로는 JPA/하이버네이트를 사용하는 바운디드 컨텍스트가 있을 수 있다.
바운디드 컨텍스트는 UI를 가지지 않을 수도 있고, 또는 UI 서버를 통해 간접적으로 브라우저와 통신할 수도 있다.
9.4 바운디드 컨텍스트 간 통합
앞서 조직 구조에 따라 컨텍스트가 결정된다고 언급했다.
그렇다면 개발 도중 조직 구조가 변화한다면 어떻게 해야 할까? 또는 두 팀이 관련된 바운디드 컨텍스트를 개발한다면 어떻게 해야 할까?
이때 필요한 것이 바운디드 컨텍스트 간의 통합이다.
예를 들어 다음의 상황이 있을 수 있다.
온라인 쇼핑 사이트에서 매출 증대를 위해 카탈로그 하위 도메인에 개인화 추천 기능을 도입한다. 기존 카탈로그 시스템을 개발하던 팀과 별도로 추천 시스템을 담당하는 팀이 새로 생겨서 이 팀에서 주도적으로 추천 시스템을 만든다.
이 경우 카탈로그 하위 도메인에서 기존 카탈로그를 위한 바운디드 컨텍스트와 추천 기능을 위한 바운디드 컨텍스트가 생긴다.
이때 카탈로그와 추천 바운디드 간 통합이 필요한 기능은 다음과 같다.
- 사용자가 제품 상세 페이지를 볼 때, 보고 있는 상품과 유사한 상품 목록을 하단에 보여준다.
사용자가 카탈로그 바운디드 컨텍스트에 추천 제품 목록을 요청하면 카탈로그 바운디드 컨텍스트는 추천 바운디드 컨텍스트로부터 추천 정보를 읽어와 추천 제품 목록을 제공한다.
이때 카탈로그 컨텍스트와 추천 컨텍스트의 도메인 모델은 서로 다르다.
카탈로그는 제품을 중심으로 도메인 모델을 구현하지만 추천은 추천 연산을 위한 모델을 구현한다.
카탈로그 시스템은 추천 시스템으로부터 추천 데이터를 받아오고, 카탈로그 시스템에서는 도메인 모델이 아닌 카탈로그 도메인 모델을 사용해서 추천 상품을 표현한다. 즉 카탈로그의 모델을 기반으로 하는 도메인 서비스를 이요해서 상품 추천 기능을 표현한다. 이런 외부 연동을 위한 도메인 서비스를 구현한 클래스는 인프라스트럭처 영역에 위치하고, 해당 구현 클래스는 도메인 모델과 외부 시스템 간의 모델 변환을 처리한다.
위의 그림에서 RecSystemclient는 외부 추천 시스템이 제공하는 REST API를 이용해서 특정 상품을 위한 추천 상품 목록을 로딩하는데, 이 API가 제공하는 데이터는 추천 시스템 모델을 기반으로 하고 있어서 카탈로그 도메인 모델과 일치하지 않는 데이터를 제공할 것이다. 따라서 RecClientSystem은 REST API 로부터 데이터를 읽어와 카탈로그 도메인에 맞는 상품 모델로 변환한다.
만약 모델 간의 변환이 복잡하다면 별도의 변환기를 두어 구현한다.
REST API를 호출하는 것은 두 바운디드 컨텍스트를 직접 통합하는 방법이다. 반대로 간접적으로 통합하는 방법도 있는데, 대표적으로 메시지 큐를 사용하는 것이 있다.
추천 시스템은 사용자가 조회한 상품 이력이나 구매 이력과 같은 사용자 활동 이력을 필요로 하는데, 이 내역을 전달할 때 메시지 큐를 사용할 수 있다.
메시지 큐는 비동기로 메시지를 처리한다. 따라서 카탈로그 바운디드 컨텍스트는 메시지를 큐에 추가한 후 추천 바운디드 컨텍스트가 해당 메시지를 처리하기까지를 기다리기 않고 바로 이어서 자신의 처리를 계속한다. 그리고 추천 바운디드 컨텍스트는 큐에서 이력 메시지를 읽어와 추천을 계산하는데 사용한다.
이런 구현을 위해서는 두 바운디드 컨텍스트가 사용할 메시지의 데이터 구조를 맞춰야 한다. 어떤 도메인 관점에서 모델을 사용하느냐에 따라 두 바운디스 컨텍스트의 구현 코드가 달라진다. 두 바운디드 컨텍스트를 개발하는 팀은 메시징 큐에 담을 데이터의 구조를 협의하게 되는데 그 큐를 누가 제공하느냐에 따라 데이터 구조가 결정된다.
9.5 바운디드 컨텍스트 간 관계
두 바운디드 컨텍스트는 다양한 방식으로 관계를 맺는다. 대표적으로 다음과 같은 유형이 있다.
- 고객 / 공급자 관계
- 공유 커널
- 독립 방식
바운디드 컨텍스트는 어떤 식으로든 연결되기 때문에 다양한 방식으로 관계를 맺는다. 가장 흔한 관계는 한쪽에서 API를 제공하고 다른 한쪽에서 그 API를 호출하는 관계(=고객/공급자 관계)이다. 대표적으로는 REST API 가 있다. 이 관계에서 API를 사용하는 바운디드 컨텍스트는 API를 제공하는 바운디드 컨텍스트에 의존한다.
1. 고객/공급자 관계를 갖는 바운디드 컨텍스트
하류 컴포넌트는 상류 컴포넌트가 제공하는 데이터과 기능에 의존한다.
카탈로그 컨텍스트는 추천 컨텍스트가 제공하는 데이터와 기능에 의존한다. 카탈로그는 추천 상품을 보여주기 위해 추천 바운디드 컨텍스트가 제공하는 REST API를 호출한다. 추천 시스템이 제공하는 REST API 의 인터페이스가 바뀌면 카탈로그 시스템의 코드도 바뀌게 된다.
상류 컴포턴트는 일종의 서비스 공급자 역할을 하고, 하류 컴포넌트는 그 서비스를 사용하는 고객 역할을 한다. 고객과 공급자 관계에 있는 두 팀은 상호 협력이 필수적인데, 두 팀은 개발 계획을 서로 공유하고 일정을 협의해야 한다.
상류 팀이 마음대로 API를 변경하면 하류 팀은 변경된 API에 맞추느라 우선순위가 높은 다른 기능을 개발하지 못하게 될 수 있다. 또 상류 팀이 뭔가를 변경할 때마다 하류 팀으로부터의 승낙을 받아야 한다면 상류 팀은 새로운 개발 시도 자체를 못할 수도 있다.
상류 컴포넌트는 보통 하류 컴포넌트가 사용할 수 있는 통신 프로토콜을 정의하고 이를 공개한다. 만약 상류 팀의 고객인 하류 팀이 다수 존재하면 상류 팀은 여러 하류 팀의 요구사항을 수정할 수 있는 API를 만들고 이를 서비스 형태로 공개해서 서비스 일관성을 유지할 수 있는데, 이런 서비스를 공개 호스트 서비스(OPEN HOST SERVICE) 라고 한다.
공개 호스트 서비스의 대표적인 예로 검색이 있다. 블로그, 카페 등과 같은 서비스를 제공하는 포털은 각 서비스별로 검색 기능을 구현하기보다는 검색을 위한 전용 시스템을 구축하고 검색 시스템과 각 서비스를 통합한다. 이때 검색 시스템은 상류 컴포넌트가 되고 블로그, 카페는 하류 컴포턴트가 된다. 상류 팀은 하류 컴포넌트의 요구사항을 수용하는 단일 API를 만들어 공개하고, 하류 팀은 공개된 API를 사용해 검색 기능을 구현한다.
상류 컴포넌트의 서비스는 상류 바운디드 컨텍스트의 도메인 모델을 따른다. 따라서 하류 컴포넌트는 상류 서비스의 모델이 자신의 도메인 모델에 영향을 주지 않도록 보호해주는 완충 지대(=안티코럽션 계층. AntiCorruption Layer)를 만들어야 한다.
2. 공유 커널
두 바운디드 컨텍스트가 같은 모델을 공유하는 경우, 공유 커널이라고 부른다. 공유 커널을 사용하면 중복을 줄일 수 있다. 하지만 두 팀이 한 모델을 공유하기 때문에 두 팀이 밀접한 관계를 유지하는 것이 매우 중요하다.
운영자를 위한 주문 관리 도구를 개발하는 팀과 고객을 위한 주문 서비스를 개발하는 팀이 다르다고 했을 때, 두 팀은 주문을 표현하는 모델을 공유하여 주문과 관련한 중복 설계를 막을 수 있다.
3. 독립 방식
독립 방식은 서로 통합하지 않는 방식을 의미한다. 두 바운디드 컨텍스트 간에 통합하지 않으므로 서로 독립적으로 모델을 발전시킨다. 독립 방식에서 두 바운디드 컨텍스트 간의 통합은 수동으로 이루어진다. 수동으로 통합하는 방식이 나쁜 것은 아니지만 규모가 커짐에 따라 한계가 분명하다. 따라서 규모가 커진다면 두 바운디드 컨ㅌ첵스트를 통합해야 하는데, 이때 두 바운디드 컨텍스트를 통합해주는 별도의 시스템을 만들어야 할 수도 있다.
9.6 컨텍스트 맵
컨텍스트 맵은 시스템의 전체 구조를 보여준다. 이는 하위 도메인과 일치하지 않는 바운디드 컨텍스트를 찾아 도메인에 맞게 바운디드 컨텍스트를 조절하고 사업의 핵심 도메인을 위해 조직 역량을 어떤 바운디드 컨텍스트에 집중할지 파악하는 데 도움을 준다.