도메인 주도 설계 - 도메인 영역의 구성요소

2022. 1. 6. 18:50도메인주도설계

스프링을 사용하는 백엔드 개발자들에게 컨트롤러와 서비스란 아주 자연스러운 웹 어플리케이션 아키텍처의 구성요소일 것 입니다.

컨트롤러와 서비스, 레포지터리를 구현하면 자연스럽게 웹 어플리케이션이 개발이 됩니다.

이처럼 도메인 주도 설계에서도 도메인 영역을 구성하는 대표적인 유형들의 객체가 있습니다.

기존 객체 모델링은 자유도가 높아 문제 영역을 파고들수록 여러 층의 복잡한 계층 구조를 만들게 될 가능성이 높습니다.

DDD에서는 도메인 모델링 구성 요소의 역할에 따른 유형을 정의하고 이러한 규칙에 따라 도메인을 모델링하면 단순하게 설계가 가능합니다.

엔티티(Entity)

엔티티 구조

  • 엔티티는 다른 엔티티와 구별할 수 있는 식별자를 가진 도메인의 실체 개념을 표현하는 객체입니다.
  • 식별자는 고유하되 엔티티의 속성 및 상태는 계속 변할 수 있습니다.
  • 식별자가 있기 때문에 추적가능합니다.
  • 자신의 라이프사이클을 가집니다.

값타입 객체(Value Object)

값객체는 각 속성이 개별적으로 변화하지 않는 개념적 완전성을 모델링한 객체입니다.

<도메인 주도 설계 핵심>의 저자 반 버논은 값객체의 특성을 다음과 같이 정의하고 있습니다.

* 도메인 내의 어떤 대상을 측정하고, 수량화하고, 설명한다.
* 관련 특징을 모은 필수 단위로 개념적 전체를 모델링한다.
* 측정이나 설명이 변경될 땐 완벽히 대체 가능하다.
* 다른 값과 등가성을 사용해 비교할 수 있다.
* 값 객체는 일단 생성되면 변경할 수 없다.

따라서,,

  • 밸류 타입은 불변 객체로 만들어야합니다.
  • 시스템이 성숙함에 따라 데이터 값을 객체로 대체해야 합니다.(원시값 포장, 일급 컬렉션)
  • 밸류 객체의 값을 변경하는 방법은 새로운 밸류 객체를 할당하는 것뿐이다.
  • 정말 String으로 우편 번호를 표현할 수 있을까요?
  • 항상 equals() 메서드를 오버라이드할 것을 권고합니다.

equals를 재정의하려거든 hashCode도 재정의하라 - Effective Java
VALUE OBJECT는 DTO가 아니다. - Martin Fowler

도메인 모델과 DB 관계형 모델의 차이
이 두 모델의 가장 큰 차이점은 도메인 모델의 엔티티는 데이터와 함께 도메인 기능을 제공한다는 점입니다.
또 다른 차이점은 도메인 모델의 엔티티는 두 개 이상의 데이터가 개념적으로 하나인 경우 밸류 타입을 이용해서 표현할 수 있는 점입니다.

DB 관계형 모델에서는 밸류 타입을 제대로 표현할 수 없습니다.
RDBMS는 밸류를 제대로 표현하기 힘들다.
주문자(ORDERER)를 모델링한 값타입 객체를 관계형 데이터베이스에 표현하려고 하면 위와 같이 두 가지 방법이 있습니다.
왼쪽 테이블의 경우 주문자(Orderer)라는 개념이 드러나지 않고 주문자 개별 데이터만 드러납니다.
오른쪽 테이블의 경우 주문자를 별도의 테이블에 저장했지만, 해당 테이블은 FK가 추가되고 각 데이터들이 하나의 PK를 구성하면서 식별가능한 엔티티에 가까워 집니다. 값타입은 식별과 관계없이 해당 값에만 집중해야합니다.

애그리거트(Aggregate)

도메인이 커질수록 개발할 도메인 모델도 커지면서 많은 엔티티와 밸류가 출현합니다. 모델이 복잡해지면 개발자가 전체 구조가 아닌 한 개의 엔티티와 밸류에만 집중하게 되는 경우가 발생합니다. 이 때 도메인 모델을 개별 객체뿐만 아니라 상위 수준에서 모델을 볼 수 있어야 전체 모델의 관계와 개별 모델을 이해하는데 도움이 됩니다.

애그리거트 기준으로 모델간의 관계 파악

  • 애그리거트는 관련 객체를 하나로 묶은 군집입니다.
  • 애그리거트 단위로 애그리거트간의 관계, 애그리거트 안에서 개별 객체간의 관계를 파악할 수 있습니다.
  • 애그리거트는 응집력을 유지하고 애그리거트간에는 느슨한 결합을 유지합니다.(직접 참조와 ID 간접 참조)
  • 애그리거트에 속한 객체는 유사하거나 동일한 라이프사이클을 갖습니다.
  • 한 애그리거트에 속한 객체는 다른 애그리거트에 속하지 않습니다.
  • 애그리거트는 군집에 속한 객체들을 관리하는 루트 엔티티를 갖습니다.
  • 대부분의 애그리거트에는 하나의 엔티티가 존재하고 드물게 두 개의 엔티티가 존재합니다.(만약 엔티티가 여러개일 경우, 해당 엔티티가 정말 값타입이 아니라 엔티티인지 의심해야합니다. 혹은 다른 애그리거트인지 의심해봐야합니다.)
  • 처음 도메인을 설계할 때, 애그리거트를 실제보다 큰 범위로 생각하고 설계했다가 도메인 지식이 쌓이면서 애그리거트가 분리되는 경우가 많습니다. (애그리거트를 분리할 수 있으면 분리해라.)
Aggregate를 애그리게이트라고 표기하는 경우가 많은데 이는 동사 발음에 해당됩니다.
명사 발음은 애그리거트라고 발음하는게 맞습니다.

애그리거트 구성

애그리거트 루트(Aggregate root)

  • 애그리거트 내에 있는 엔티티 중 가장 상위의 엔티티를 애그리거트 루트로 정합니다.
  • 애그리거트 루트를 통해서만 간접적으로 애그리거트 내의 엔티티나 값 객체를 변경할 수 있습니다.(캡슐화, 퍼사드 패턴과 유사)
  • 애그리거트간의 참조는 직접참조하지 않고 ID참조를 합니다.(그림에서 Order가 Buyer를 직접 참조하지 않고 BuyerID로 참조하고 있다.)
  • 애그리거트는 단일 트랜잭션으로 일관성을 유지하지만 애그리거트간의 일관성이 필요하다면 도메인 이벤트를 통해 다른 애그리거트를 갱신해서 일관성을 유지합니다.

레포지터리(Repository)

  • 엔티티나 밸류가 요구사항에서 도출되는 도메인 모델이라면 리포지터리는 구현을 위한 도메인 모델입니다.
  • 애그리거트 단위로 도메인 객체를 저장하고 조회하는 기능을 정의합니다.
  • 레포지터리는 도메인 객체를 영속화하는 데 필요한 기능을 추상화한 것으로 고수준 모듈에 속합니다.
  • 애그리거트를 구하는(조회하는) 리포지터리 메서드는 완전한 애그리거트를 제공해야 한다.
  • 리포지터리가 완전한 애그리거트를 제공하지 않으면, 필드나 값이 올바르지 않아 애그리거트의 기능을 실행하는 도중에 NullPointerException과 같은 문제가 발생하게 된다.
  • 리포지토리는 애그리거트(루트) 단위로 존재하며 테이블 단위로 존재하는 것이 아니다.(애그리거트 1 개 당 리포지토리 1 개)

도메인 서비스

한 애그리거트에 넣기 애매한 도메인 개념을 구현하려면 어떻게 해야할까요?

특정 애그리거트에 억지로 넣기보다는 도메인 서비스를 이용해서 도메인 개념을 명시적으로 드러내면 됩니다.

예) 결제 금액을 계산하는 로직은 어떤 애그리거트에서 수행되야할까요?

상품 애그리거트: 구매하는 상품의 가격이 필요하다. 또한 상품에 따라 배송비가 추가되기도 한다.
주문 애그리거트: 상품별로 구매 개수가 필요하다.
할인 쿠폰 애그리거트: 쿠폰별로 지정한 할인 금액이나 비율에 따라 주문 총 금액을 할인한다. 할인 쿠폰을 조건에 따라 중복 사용할 수 있다거나 지정한 카테고리의 상품에만 적용할 수 있다는 제약 조건이 있다면 할인 계산이 복잡해진다.
회원 애그리거트: 회원 등급에 따라 추가 할인이 가능하다.

결론: 결제 금액은 상품의 가격과 상품별 구매 개수, 배송비, 할인 쿠폰, 회원등급에 따른 할인 등 다양한 애그리거트를 참조하여 결정되야합니다. 이렇듯 특정 애그리거트에 속하지 못하고 여러 애그리거트가 협동하여 풀어야할 비즈니스 로직은 도메인 서비스에 구현하면 됩니다.
  • 응용 영역의 서비스가 용용 로직을 다룬다면 도메인 서비스는 도메인 로직(비즈니스 로직)을 다룹니다.
  • 도메인 영역의 애그리거트나 밸류와 같은 다른 구성요소와 비교할 때 다른 점은, 상태 없이 로직만 구현되는 것입니다.
  • 서비스를 사용하는 주체는 애그리거트가 될 수도 있고 응용 서비스가 될 수도 있습니다.
  • 특정 기능이 응용 서비스인지 도메인 서비스인지 감을 잡기 어려울 때는 해당 로직이 애그리거트의 상태를 변경하거나 애그리거트의 상태 값을 계산하는지 검사해 보면 됩니다.

팩토리

  • 복잡한 객체를 생성해야한다면 생성 역할만 책임지는 Factory에게 그 역할을 위임할 수 있습니다.
  • 도메인 객체를 생성하기 위한 기존의 클라이언트 코드가 깔끔해집니다.

팩토리의 생성 위치

  1. 애그리거트 루트의 정적 팩토리 메서드 - 애그리거트를 생성하기 좋은 위치
  2. 도메인 서비스 - 다른 애그리거트를 이용하여 생성해야한다면 고려해보자
  3. 별개의 팩토리 클래스 - 구상 구현체나 생성과정의 복잡성등을 감춰야 하는 경우

Factory는 자신의 생성물과 가장 밀접한 관계에 있는 위치에 있어야 합니다. (만들어내는 객체와 매우 강하게 결합돼 있으므로)

도메인 이벤트

  • 도메인 이벤트를 사용하면 애그리거트간, 바운디드 컨텍스트간, 외부 서비스와의 결합을 느슨하게 합니다. (도메인 로직이 섞이는 것을 방지 할 수 있습니다.)
  • 도메인 모델에서 이벤트 발생 주체는 엔티티, 밸류, 도메인 서비스와 같은 도메인 객체입니다.
  • 도메인 객체는 도메인 로직을 실행해서 상태가 바뀌면 관련 이벤트를 발생시킵니다.
  • 서비스간 정합성을 일치시키기 위해 단위 애그리거트의 주요 상태 값을 담아 전달되도록 모델링합니다.
  • 이벤트의 용도는 후처리, 데이터 동기화 등
  • 스프링에서 제공하는 AbstractAggregateRoot를 애그리거트 루트에 확장하면 애그리거트 루트 내에서 직접 이벤트를 발생할 수 있습니다.
  • 이벤트 핸들러는 응용 영역에서 구현하면 됩니다.