Blog

도메인 주도 개발 시작하기(9)

도메인 주도 개발 시작하기(9)

9-1. 도메인 모델과 경계

하위 도메인마다 같은 용어라도 의미가 다르고, 같은 대상이라도 지칭하는 용어가 다를 수 있기 때문에 한 개의 모델로 모든 하위 도메인을 표현하려는 시도는 올바른 방법이 아니며 표현할 수도 없다
올바른 도메인 모델을 개발하려면 하위 도메인마다 모델을 만들어야 한다
각 모델은 명시적으로 구분되는 경계를 가져서 섞이지 않도록 해야 한다
여러 하위 도메인 모델이 섞이기 시작하면 모델의 의미가 약해질 뿐만 아니라 여러 도메인의 모델이 서로 얽히기 때문에 각 하위 도메인별로 다르게 발정하는 요구사항을 모델에 반영하기 어려워 진다
모델은 특정한 컨텍스트(문맥) 하에서만 완전한 의미를 갖는다
이렇게 구분되는 경계를 갖는 컨텍스트를 바운디드 컨텍스트(Bounded Context) 라고 부른다

9-2. 바운디드 컨텍스트

바운디드 컨텍스트는 모델의 경계를 결정하며, 논리적으로 한 개의 모델을 갖는다
바운디드 컨텍스트는 용어 를 기준으로 구분한다
예시) 카탈로그 컨텍스트 와 재고 컨텍스트는 서로 다른 용어를 사용하므로 이 용어를 기준으로 컨텍스트를 분리할 수 있다
바운디드 컨텍스트는 실제로 사용자에게 기능을 제공하는 물리적 시스템으로 도메인 모델은 이 바운디드 컨텍스트 안에서 도메인을 구현한다
이상적으로 하위 도메인과 바운디드 컨텍스트가 1:1 관계를 가지면 좋겠지만, 현실은 그렇지 않을 때가 많다
바운디드 컨텍스트는 기업의 팀 조직 구조에 따라 결정되기도 한다
여러 하위 도메인을 하나의 바운디드 컨텍스트에서 개발할 때 주의할 점은 하위 도메인의 모델이 섞이지 않도록 하는 것이다
도메인 모델이 개별 하위 도메인을 제대로 반영하지 못해서 하위 도메인별로 기능을 확장하기 어렵게 되고, 이는 서비스 경쟁력을 떨어뜨리는 원인이 된다
물리적인 바운디드 컨텍스트가 한 개이더라도 내부적으로 패키지를 활용해서 논리적으로 바운디드 컨텍스트를 만든다

9-3. 바운디드 컨텍스트 구현

바운디드 컨텍스트가 도메인 모델만 포함하는것은 아니다
도메인 기능을 사용자에게 제공하는 데 필요한 표현 영역, 응용 서비스, 인프라스트럭처 영역을 모두 포함한다
도메인 모델의 데이터 구조가 바뀌면 DB 테이블 스키마도 함께 변경해야 하므로 테이블도 바운디드 컨텍스트에 포함된다
모든 바운디드 컨텍스트를 반드시 도메인 주도로 개발할 필요는 없다
서비스-DAO 구조를 사용하면 도메인 기능이 서비스에 흩어지게 되지만, 도메인 기능 자체가 단순하면 코드를 유지 보수하는데 문제 되지 않는다
CQRS 패턴을 이용하면 상태 변경과 관련된 기능은 도메인 모델 기반으로 구현하고, 조회 기능은 서비스-DAO 를 이용해서 구현할 수 있다
각 바운디드 컨텍스트는 서로 다른 구현 기술을 사용할 수 있다
중간에 UI를 처리하는 서버를 두고 UI 서버에서 바운디드 컨텍스트와 통신해서 사용자 요청을 처리하는 방법도 있다
flowchart LR browser(브라우저) <-->|1. 제품 정보 요청| UI(UI 서버) UI -->|2. 요청| catalog(카탈로그 바운디드 컨텍스트) UI -->|3. 요청| review(리뷰 바운디드 컨텍스트)
Mermaid
이 구조에서 UI 서버는 각 바운디드 컨텍스트를 위한 파사드 역할을 수행한다
UI 서버는 카탈로그와 리뷰 바운디드 컨텍스트로부터 필요한 정보를 읽어와 조합한 뒤 브라우저에 응답을 제공한다

9-4. 바운디드 컨텍스트 간 통합

REST API 를 호출하는 것은 두 바운디드 컨텍스트를 직접 통합하는 방식이다.
예시)
상품 추천 기능을 표현하는 도메인 서비스
public interface ProductRecommendationService { List<Product> getRecommendationsOf(ProductId id); }
Java
도메인 서비스를 구현한 클래스는 인프라스트럭처 영역에 위치한다
이 클래스는 외부 시스템과의 연동을 처리하고 외부 시스템의 모델과 현재 도메인 모델간의 변환을 책임진다
flowchart LR service(ProductRecommendationService) --> Rec(RecSystemClient) --> component(외부 추천 시스템)
Mermaid
RecSystemClient 는 외부 추천 시스템이 제공하는 REST API 를 이용해서 특정 상품을 위한 추천 상품 목록을 로딩한다
[ { "itemId": "PROD-1000", "type": "PRODUCT", "rank": 100 }, { "itemId": "PROD-1001", "type": "PRODUCT", "rank": 54 } ]
JSON
RecSystemClient 는 REST API 로 부터 데이터를 읽어와 카탈로그 도메인에 맞는 상품 모델로 변환한다
public class RecSystemClient implements ProductRecommendationService { private ProductRepository productRepository; @Override public List<Product> getRecommendataionsOf(ProductId id) { list<RecommendataionItem> items = getRecItems(id.getValue()); return toProducts(items); } private List<RecommendataionItem> getRecItems(String itemId) { // externalRecClient 는 외부 추천 시스템을 위한 클라이언트라고 가정 return externalRecClient.getRecs(itemId); } private List<Product> toProducts(List<RecommendataionItem> items) { return items.stream() .map(item -> toProductId(item.getItemId()) .map(prodId -> productRepository.findById(prodId)) .collect(toList()); } private ProductId toProductId(String itemId) { return new ProductId(itemId); } }
Java
직접 통합하는 대신 간접적인 통합 방식도 있는데, 대표적인 간접 통합 방식이 Message Queue 를 사용하는 것이다
flowchart LR catalog(카탈로그 바운디드 컨텍스트) message(메시지 시스템) recommend(추천 바운디드 컨텍스트) catalog -->|이력을 메시지 형식으로 큐에 추가| message recommend -->|큐에서 메시지를 가져옴| message
Mermaid
Message Queue 는 비동기로 메세지를 처리한다
어떤 도메인 관점에서 모델을 사용하느냐에 따라 두 바운디드 컨텍스트의 구현 코드가 달라지게 된다
예시)
카탈로그 도메인 모델을 기준으로 메시지를 전송하므로 추천 시스템은 자신의 모델에 맞게 메시지를 변환해서 처리해야 한다
// 상품 조회 관련 로그 기록 코드 public class ViewLogService { private MessageClient messageClient; public void appendViewLog(String memberId, String productId, Date time) { messageClient.send(new ViewLog(memberId, productId, time)); } } // messageClient public class RabbitMQClient implements MessageClient { private RabbitTemplate rabbitTemplate; @Override public void send(ViewLog viewLog) { rabbitTemplate.convertAndSend(logQueueName, viewLog); } }
Java
추천 시스템을 기준으로 큐에 데이터를 저장하기로 했다면 카탈로그 쪽 코드는 다음과 같이 변경될 수 있다
// 상품 조회 관련 로그 기록 코드 public class ViewLogService { private MessageClient messageClient; public void appendViewLog(String memberId, String productId, Date time) { messageClient.send( new ActivityLog(productId, memberId, ActivitiyType.VIEW, time)); } } // mesasgeClient public class RabbitMQClient implements MessageClient { private RabbitTemplate rabbitTemplate; @Override public void send(ActivityLog activityLog) { rabbitTemplate.convertAndSend(logQueueName, activityLog); } }
Java
두 바운디드 컨텍스트를 개발하는 팀은 메시징 큐에 담을 데이터의 구조를 협의하게 되는데, 그 큐를 누가 제공하느냐에 따라 데이터 구조가 결정된다.
flowchart LR catalog(카탈로그 바운디드 컨텍스트) message(메시지 시스템) A(A 바운디드 컨텍스트) B(B 바운디드 컨텍스트) C(C 바운디드 컨텍스트) catalog -->|출판| message message -->|구독| A message -->|구독| B message -->|구독| C
Mermaid
큐를 추천 시스템에서 제공할 경우 큐를 통해 메시지를 추천 시스템에 전달하는 방식이 된다
이 경우 큐로 인해 비동기로 추천 시스템에 데이터를 전달하는 것을 제외하면, 추천시스템이 제공하는 REST API 를 사용해서 데이터를 전달하는 것과 차이가 없다

마이크로서비스와 바운디드 컨텍스트

마이크로서비스 아키텍처는 단순 유행을 지나 많은 기업에서 자리를 잡아가고 있다
마이크로서비스는 애플리케이션을 작은 서비스로 나누어 개발하는 아키텍처 스타일이다
개별 서비스를 독립된 프로세스로 실행하고 각 서비스가 REST API 나 메시징을 이용해서 통신하는 구조를 갖는다
마이크로서비스의 특징은 바운디드 컨텍스트와 잘 어울린다
각 바운디드 컨텍스트는 모델의 경계를 형성하는데 바운디드 컨텍스트를 마이크로서비스로 구현하면 자연스럽게 컨텍스트별 모델이 분리된다
코드로 생각하면 마이크로서비스마다 프로젝트를 생성하므로 바운디드 컨텍스트마다 프로젝트를 만들게 된다
코드 수준에서 모델을 분리하여 두 바운디드 컨텍스트의 모델이 섞이지 않도록 해준다
별도 프로세스로 개발한 바운디드 컨텍스트는 독립적으로 배포하고 모니터링하며 확장되는데 이 역시 마이크로서비스가 갖는 특징이다

9-5. 바운디드 컨텍스트 간 관계

바운디드 컨텍스트는 어떤 식으로든 연결되기 때문에 두 바운디드 컨텍스트는 다양한 방식으로 관계를 맺는다
일반적으로 한쪽에서 API 를 제공하고, 다른 한쪽에서 API 를 호출하는 관계이다 (REST API 가 대표적이다)
이 관계에서 API 를 사용하는 바운디드 컨텍스트는 API 를 제공하는 바운디드 컨텍스트에 의존하게 된다
flowchart LR catalog(카탈로그 바운디드 컨텍스트 \n 하류) recommend(추천 바운디드 컨텍스트 \n 상류) catalog ---|고객/공급자관계| recommend
Mermaid
하류 (downstream) 컴포넌트는 상류(upstream) 컴포넌트가 제공하는 데이터와 기능에 의존한다
상류 컴포넌트는 서비스 공급자 역할을 하며, 하류 컴포넌트는 그 서비스를 사용하는 고객 역할을 한다
상류팀과 하류팀은 개발 계획을 서로 공유하고 일정을 협의해야 한다
상류 컴포넌트는 보통 하류 컴포넌트가 사용할 수 있는 통신 프로토콜을 정의하고 이를 공개한다
상류 팀은 여러 하류 팀의 요구사항을 수용할 수 있는 API 를 만들고 이를 서비스 형태로 공개해서 서비스의 일관성을 유지할 수 있다
이러한 서비스를 가르켜 공개 호스트 서비스 (OPEN HOST SERVICE, OHS) 라고 한다
flowchart TB search(검색 바운디드 컨텍스트) blog(블로그 바운디드 컨텍스트) cafe(카페 바운디드 컨텍스트) table(게시판 바운디드 컨텍스트) search --- open(공개 호스트 서비스) subgraph 하류 컴포넌트 direction BT blog --> open cafe --> open table --> open end
Mermaid
상류 컴포넌트의 서비스는 상류 바운디드 컨텍스트의 도메인 모델을 따른다
하류 컴포넌트는 상류 서비스의 모델이 자신의 도메인 모델에 영향을 주지 않도록 보호해 주는 완충 지대를 만들어야 한다
내 모델이 깨지는 것을 막아주는 안티코럽션 계층 (Anticorruption Layer, ACL) 만들어야 한다
위 예시에 RecSystemClient 가 그 역할을 수행한다

공유 커널 (SHARED KERNEL)

두 바운디드 컨텍스트가 같은 모델을 공유하는 경우도 있다
두 팀이 공유하는 모델을 공유 커널 (SHARED KERNEL) 이라고 부른다
공유 커널의 장점은 중복을 줄여준다
두 팀이 하나의 모델을 개발해서 공유하기 때문에 두 팀에서 동일한 모델을 두 번 개발하는 중복을 줄일 수 있다
두 팀이 한 모델을 공유하기 때문에 한 팀에서 임의로 모델을 변경해서는 안되며, 두 팀이 밀접한 관계를 유지해야 한다
두 팀의 밀접한 관계를 유지할 수 없다면 공유 커널을 사용할 때의 장점보다 공유 커널로 인해 개발이 지연되고 정체되는 문제가 더 커지게 된다

독립 방식 (SEPARATE WAY)

바운디드 컨텍스트를 서로 통합하지 않는 방식이다
서로 독립적으로 모델을 발전시킨다
독립 방식에서 두 바운디드 컨텍스트 간의 통합은 수동으로 이루어진다
flowchart LR shopping(쇼핑몰 바운디드 컨텍스트) user(유저) external(외부 ERP 바운디드 컨텍스트) user -->|판매 정보를 보고| shopping user -->|ERP 에 수동 입력| external
Mermaid
수동으로 통합하는 방식이 나쁜것은 아니지만, 규모가 커질수록 수동 통합에는 한계가 있으므로 규모가 커지기 시작하면 두 바운디드 컨텍스트를 통합해야 한다
독립 방식으로 개발한 두 바운디드 컨텍스트를 통합하기 위해 별도의 시스템을 만들어야 할 수도 있다.

9-6. 컨텍스트 맵

개별 바운디드 컨텍스트에 매몰되면 전체를 보지 못할때도 있다
전체 비지니스를 조망할 수 있는 지도가 필요한데 그것이 바로 컨텍스트 맵이다
컨텍스트의 경계가 명확하게 드러나고, 서로 어떤 관계를 맺고 있는지 알 수 있다
바운디드 컨텍스트 영역에 주요 애그리거트를 함께 표시하면 모델에 대한 관계가 더 명확히 드러난다
오픈 호스트 서비스(OHS) 와 안티코럽션 계층(ACL) 와 함께 하위 도메인이나 조직 구조를 함께 표시하면 도메인을 포함한 전체 관계를 이해하는데 도움이 된다
컨텍스트 맵은 시스템 전체 구조를 보여준다
하위 도메인과 일치하지 않는 바운디드 컨텍스트를 찾아 도메인에 맞게 바운디드 컨텍스트를 조절하고 사업의 핵심 도메인을 위해 조직 역량을 어떤 바운디드 컨텍스트에 집중할지 파악하는데 도움을 준다
컨텍스트 맵을 그리는 규칙은 따로 없다