기록이 힘이다.

[도메인 주도 개발 시작하기] 9. 도메인 모델과 바운디드 컨텍스트 본문

IT서적/도메인 주도 개발 시작하기

[도메인 주도 개발 시작하기] 9. 도메인 모델과 바운디드 컨텍스트

dev22 2023. 7. 31. 20:37
728x90

도메인 모델과 경계

처음 도메인 모델을 만들 때 빠지기 쉬운 함정이 도메인을 완벽하게 표현하는 단일 모델을 만드는 시도를 하는 것이다. 그런데 1장에서 말한 것처럼 한 도메인은 다시 여러 하위 도메인으로 구분되기 때문에 한 개의 모델로 여러 하위 도메인을 모두 표현하려고 시도하면 오히려 모든 하위 도메인에 맞지 않는 모델을 만들게 된다. 

논리적으로 같은 존재처럼 보이지만 하위 도메인에 따라 다른 용어를 사용하는 경우도 있다. 

 

하위 도메인마다 사용하는 용어가 다르기 때문에 올바른 도메인 모델을 개발하려면 하위 도메인마다 모델을 만들어야 한다. 모델은 특정한 컨텍스트(문맥) 하에서 완전한 의미를 갖는다. 같은 제품이라도 카탈로그 컨텍스트와 재고 컨텍스트에서 의미가 서로 다른다. 이렇게 구분되는 경계를 갖는 컨텍스트를 DDD에서는 바운디드 컨텍스트라고 부른다. 

 

바운디드 컨텍스트

모델의 경계를 결정하며 한 개의 바운디드 컨텍스트는 논리적으로 한 개의 모델을 갖는다.

이상적으로 하위 도메인과 바운디드 컨텍스트가 일대일 관계를 가지면 좋겠지만 현실은 그렇지 않을 때가 많다. 바운디드 컨텍스트는 기업의 팀 조직 구조에 따라 결정되기도 한다. 

 

규모가 작은 기업은 전체 시스템을 한 개 팀에서 구현할 때도 있다. 

여러 하위 도메인을 하나의 바운디드 컨텍스트에서 개발할 때 주의할 점은 하위 도메인의 모델이 섞이지 않도록 하는 것이다. 

바운디드 컨텍스트 구현

도메인 기능을 사용자에게 제공하기 위해 표현 영역, 응용 서비스, 인프라 영역 등을 모두 포함한다.

도메인 모델의 구조가 바뀌면 DB 테이블 스키마도 함께 변경해야 하므로
해당 테이블도 바운디드 컨텍스트에 포함된다.

모든 바운디드 컨텍스트를 반드시 도메인 주도로 개발할 필요는 없다. 

도메인 기능 자체가 단순하면 서비스 - DAO로 구성된 CRUD 방식을 사용해도 코드를 유지 보수하는 데 문제 되지 않는다고 생각한다. 

한 바운디드 컨텍스트에서 두 방식을 혼합해서 사용할 수도 있다. 대표적인 예가CQRS 패턴이다.

CQRS(Command Query Responsibility Segregation)는 상태를 변경하는 명령 기능과 내용을 조회하는 쿼리 기능을 위한 모델을 구분하는 패턴이다.

각 바운디드 컨텍스트는 서로 다른 구현 기술을 사용할 수도 있다. 

 

바운디드 컨텍스트 간 통합

-사용자가 제품 상세 페이지를 볼 때, 보고 있는 상품과 유사한 상품 목록을 하단에 보여준다. 

 

카탈로그 시스템은 추천 시스템에서 받은 상품 모델을 사용하기보다는 카탈로그 도메인 모델을
사용해서 추천 상품을 표현해야 한다.

즉 다음과 같이 카탈로그의 모델을 기반으로 하는 도메인 서비스를 사용해서 추천 기능을 표현해야 한다.

// 상품 추천 기능을 표현하는 도메인 서비스
public interface ProductRecommendationService {
  List<Product> getRecommendationOf(ProductId id);
}

도메인 서비스를 구현한 클래스는 인프라스트럭처 영역에 위치한다.
이 클래스는 외부 시스템과의 연동을 처리하고 외부 시스템의 모델과 현재 도메인 모델 간의 변환을 책임진다.

RecSystemClient 가 REST API 를 통해 데이터를 읽어오고 카탈로그에 맞는 도메인으로 변환한다.
아래는 해당 코드를 나타낸 것이다.

public class RecSystemClient implements ProductRecommendationService {

  private ProductRepository productRepository;

  @Override
  public List<Product> getRecommendationOf(ProductId id) {
    List<Recommendation> items = getRecItems(id.getValue());
    return toProducts(items);
  }

  private List<RecommendationItem> getRecItems(String itemId) {
    return externalRecClient.getRecs(itemId);
  }

  private List<Product> toProducts(List<RecommendationItem> 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);
  }

}

REST API 호출은 두 바운디드 컨텍스트를 직접 통합하는 방법이다.

간접적으로 통합하는 대표적인 방식은 메세지 큐를 사용하는것이다.

 

메세징 큐에 담을 구조를 협의할때 그 큐를 누가 제공하느냐에 따라 데이터 구조가 결정된다.
즉, 한쪽에서 메세지를 출판하고 다른 쪽에서 구독하는 출판/구독 모델을 따른다.

 

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

마이크로서비스는 애플리케이션을 작은 서비스로 나누어 개발하는 아키텍처 스타일이다. 개별 서비스를 독립된 프로세서로 실행하고 각 서비스가 REST API나 메시징을 이용해서 통신하는 구조를 갖는다. 

 

이런 마이크로서비스의 특징은 바운디드 컨텍스트와 잘 어울린다. 각 바운디드 컨텍스트는 모델의 경계를 형성하는데 바운디드 컨텍스트를 마이크로서비스로 구현하면 자연스럽게 컨텍스트별로 모델이 분리된다. 

 

별도 프로세스로 개발한 바운디드 컨텍스트는 독립적으로 배포하고 모니터링하며 확장되는데 이 역시 마이크로서비스가 갖는 특징이다. 

 

바운디드 컨텍스트 간 관계

REST API가 대표적이다. 

상류 컴포넌트는 일종의 서비스 공급자 역할을 하며 하류 컴포넌트는 그 서비스를 사용하는 고객 역할을 한다. 상류 팀과 하류 팀은 개발 계획을 서로 공유하고 일정을 협의해야 한다. 

 

공개 호스트 서비스(OPEN HOST SERVICE)

상류 팀의 고객인 하루 팀이 다수 존재하면 상류 팀은 여러 하류팀의 요구사항을 수용할 수 있는 API 를 만들고 이를 서비스 형태로 공개하여 일관성을 유지할수 있다.

공개 호스트 서비스의 대표적인 예는 검색이다.
이때 상류 컴포넌트는 검색 시스템이고, 하류 컴포넌트는 블로그, 카페, 게시판등이 된다.

전체 비지니스를 조망할 수 있는 지도가 필요한데 그것이 바로 컨텍스트 맵이다.