도메인 주도 개발 시작하기(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) 와 함께 하위 도메인이나 조직 구조를 함께 표시하면 도메인을 포함한 전체 관계를 이해하는데 도움이 된다
•
컨텍스트 맵은 시스템 전체 구조를 보여준다
◦
하위 도메인과 일치하지 않는 바운디드 컨텍스트를 찾아 도메인에 맞게 바운디드 컨텍스트를 조절하고 사업의 핵심 도메인을 위해 조직 역량을 어떤 바운디드 컨텍스트에 집중할지 파악하는데 도움을 준다
◦
컨텍스트 맵을 그리는 규칙은 따로 없다