도메인 주도 개발 시작하기 (9) - 도메인 모델과 바운디드 컨텍스트

Jaewoo Ha·2022년 12월 12일
0

9.1 도메인 모델과 경계

  • 처음부터 도메인을 완벽하게 표현하려는 단일 모델을 만드는 시도를 하는 함정에 빠지기 쉽다.
  • 한 개의 모델로 여러 하위 도메인을 모두 표현하려고 시도하면 오히려 모든 하위 도메인에 맞지 않는 모델을 만들게 된다.

경계 설정 방법

  • 논리적으로 같은 존재처럼 보이지만 하위 도메인에 따라 다른 용어를 사용하는 경우도 있다.
  • 하위 도메인마다 사용하는 용어가 다르기 때문에 하위 도메인마다 모델을 만들어야된다.
  • 모델으 ㄴ특정한 컨텍스트 하에서 완전한 의미를 갖는다.

9.2 바운디드 컨텍스트

  • 논리적으로 한 개의 모델을 갖는다. 용어를 기준으로 구분한다.
  • 실제로 사용자에게 기능을 제공하는 물리적 시스템으로 도메인은 바운디드 컨텍스트 안에서 도메인을 구현한다.

이상적으로 하위 도메인과 바운디드 컨텍스트가 일대일 관계를 가지면 좋겠지만 현실은 그렇지 않을 때가 많다.

설정 방법

  • 팀과 기업의 상황에 따라 바운디드 컨텍스트를 잡을 수 있다.

여러 하위 도메인을 하나의 바운디드 컨텍스트에서 개발할 때 주의할 점

  • 하위 도메인의 모델이 섞이지 않도록 한다. -> 도메인 모델이 개별 하위 도메인을 제대로 반영하지 못해서 하위 도메인별로 기능을 확장하기 어렵다. 서비스 경쟁력을 떨어뜨린다.

구현 방법

  • 한 개의 바운디드 컨텍스트가 여러 하위 도메인을 포함하더라도 하위 도메인마다 구분되는 패키지를 갖도록 한다.
  • 하위 도메인을 위한 모델이 서로 뒤섞이지 않고 하위 도메인마다 바운디드 컨텍스트를 갖는 효과를 낸다.

  • 구현하는 하위 도메인에 알맞는 모델을 포함한다. ex) 같은 사용자라 하더라도 주문 바운디드 컨텍스트와 회원 바운디드 컨텍스트가 갖는 모델이 달라진다.

9.3 바운디드 컨텍스트 구현

  • 도메인 모델만 포함하는 것은 아니며, 표현 영역, 응용 서비스, 인프라스트럭쳐 영역을 모두 포함한다. 또한 DB 테이블 스키마도 포함한다.

구현 방법

  1. 모든 바운디드 컨텍스트를 반드시 도메인 주도로 개발할 필요는 없다. 복잡한 도메인 로직을 갖지 않는다면 CRUD방식으로 구현해도 된다.

  2. 한 바운디드 컨텍스트에서 두 방식을 혼합해서 사용할 수 있다. ex) CQRS: 단일 바운디드 컨텍스트에 적용한다면 상태 변경과 관련된 기능은 도메인 모델 기반으로 구현하고 조회 기능은 서비스 - DAO를 이용해서 구현한다.

  3. 서로 다른 구현 기술을 사용할 수 있다.

결론

  • 바운디드 컨텍스트가 반드시 사용자에게 보여지는 UI를 가지고 있어야 하는 것은 아니다.
  • UI를 처리하는 서버를 두고 UI서버에서 바운디드 컨텍스트와 통신해서 사용자 요청을 처리하는 방법도 있다.

9.4 바운디드 컨텍스트 간 통합

  • 카탈로그 하위 도메인에 개인화 추천 기능을 도입을 한다면 기존 카탈로그 시스템을 개발과 추천 시스템 개발을 병렬로 진행을 할 수 있다.

다른 바운디드 컨텍스트간 통합이 발생할 수 있다.

추천 시스템은 상품의 정보를 포함하지 않으며 상품 번호 대신 아이템 ID라는 용어를 사용해서 식별자를 표현하고 추천 순위와 같은 데이터를 담는다. 즉, 카탈로그의 모델을 기반으로 하는 도메인 서비스를 이용해서 상품 추천 기능을 표현해야 한다.

interface ProductRecommendationService {
    
    fun getRecommendationOf(id: ProductId): List<Product>
}
  • 도메인 서비스를 구현한 클래스는 인프라스트럭처 영역에 위치한다. 외부 시스템과의 연동을 처리하고 외부 시스템의 모델과 현재 도메인 모델 간의 변환을 책임진다.

외부 시스템을 통해 카탈로그 도메인에 맞느 상품 모델로 변환한다.

class RecSystemClient(private val productRepository: ProductRepository): ProductRecommendationService {

    override fun getRecommendationOf(id: ProductId): List<Product> {
        val items = getRecItems(id.value)
        return toProducts(items)
    }

    private fun getRecItems(itemId: String): List<RecommendationItem> {
    	// externalRecClient는 외부 추천 시스템에 연결할 때 사용하는 클라이언트로 가정한다.
        return extenalRecClient.getRecs(itemId)
    }

    private fun toProducts(items: List<RecommendationItem>): List<Product> {
        return items.stream()
            .map { item -> toProductId(item.itemId) }
            .map { prodId -> productRepository.findById(prodId) }
            .collect(Collection::toList)
    }

    private fun toProductId(itemId: String): ProductId {
        return ProductId(itemId)
    }
}

두 모델 간의 변환 과정이 복잡하면 변환 처리를 위한 별도 클래스를 만들고 이 클래스에서 변환을 처리해도 된다.

간접적으로 두 바운디드 컨텍스트를 통합하는 방법도 있다.

  • 메세지 큐를 사용한다면 간접적으로 통합이 가능하다.

ViewLogService에서는 상품 조회 관련 로그 기록 코드를 남긴다.

class ViewLogService(private val messageClient: MessageClient) {
    
    fun appendViewLog(memberId: String, productId: String, time: LocalDateTime) {
        messageClient.send(ViewLog(memberId, productId, time))
    }
}

RabbitMQClient 는 메세지를 담을 수 있는 인프라스트럭쳐의 영역이다.

class RabbitMQClient(private val rabbitTemplate: RabbitTemplate): MessageClient {

    override fun send(viewLog: ViewLog) {
        rabbitTemplate.convertAndSend(logQueueName, viewLog)
    }
}

결론

두 바운디드 컨테스트를 개발을 한다면 데이터의 구조를 협의하게 되는데 그 큐를 누가 제공하느냐에 따라 데이터 구조가 결정된다.한쪽에서 메세지를 출판하고 다른 쪽에서 메세지를 구독하는 출판/구독 모델을 따른다.

9.5 바운디드 컨텍스트 간 관계

  • 바운디드 컨텍스트는 어떤 식으로든 연결되기 때문에 다양한 방식으로 관계를 맺는다.
  • 하류 컴포넌트는 상류 컨포넌트가 제공하는 데이터와 기능에 의존한다.
  • 상/하류 컴포넌트의 상호작용은 필수적이다.

상류 컴포넌트

  • 일종의 서비스 공급자 역할
  • 하류 컴포넌트가 사용할 수 있는 통신 프로토콜을 정의하고 공개한다.
  • 서비스 형태로 하류 컴포넌트에게 제공을 하며 서비스의 일관성을 유지한다. -> 호스트 서비스

하류 컴포넌트

  • 서비스를 사용하는 고객 역할
  • 상류 컴포넌트의 서비스는 상류 바운디드 컨텍스트의 도메인 모델을 따르므로 상류 서비스의 모델이 자신의 도메인 모델에 영향을 주지 않도록 보호해주는 완충 지대가 필요하다.


공개 호스트 서비스 예시 👆

공유 커널

  • 두 개의 바운디드 컨텍스트가 공유하는 모델은 공유 커널이라 한다.

장점

  • 중복을 줄여준다.

단점

  • 한 바운디드 컨텍스트에서 임의로 모델을 변경하면 안되며 밀접한 관계를 유지해야된다.

독립 방식

  • 두 바운디드 컨텍스트가 통합하지 않으며 독립적으로 모델을 발전시킨다.
  • 컨텍스트 간의 통합은 수동으로 이루어진다.
  • 규모가 커질수록 수동 통합에는 한계가 있다.

9.6 컨텍스트 맵

  • 나무만 보고 숲을 보지 못하는 상황을 방지하기 위해선 전체 비즈니스를 조망할 수 있는 지도를 그려야된다.

  • 바운디드 컨텍스트 영역에 주요 애그리거트를 함께 표시하면 모델의 대한 관계가 더 명확히 드러난다.
  • 시스템의 전체 구조를 보여준다.
  • 하위 도메인과 일치하지 않는 바운디드 컨텍스트를 찾아 도메인에 맞게 바운디드 컨텍스트를 조절하고 사업의 핵심 도메인을 위해 조직 역량을 어떤 바운디드 컨텍스트에 집중할 지 파악하는데 도움을 준다.
profile
내일의 코드는 더 안전하고 깔끔하게

0개의 댓글