DDD tactical components

VO Entity Domain Service Domain Events Aggregate

DDD에서 가장 중심이 되는 부분이 바로 Vo와 Entity이다.
Entity는 Vo와 identity를 기준으로 구분되곤 하는데, 실제 설계를 하다보면 entity의 범위는 어디까지인지 aggregate과 entity와의 관계 등이 나에게는 모호해서 entity를 더 공부하게 됐다.

Vaughn Vernon의 Implement Domain Driven DesignEntity 챕터를 다시 읽게 되었는데 DDD를 적용하는 프로젝트를 몇 건 진행한 후에 다시 보니 보이는 것들이 있었다.

Entity의 가장 중요한 부분은 identity를 갖는 것이지만 그게 entity의 전부는 아니다.
ID 부터 우선 설계해아한다는 것, constructor에 query에 사용되는 parameter들이 모두 포함되어야 한다는 것 등을 배웠다.

DDD를 얕게 아는 대부분의 사람들이 “entity는 id가 있고 vo는 없어” 정도로 이해하고 말곤 하지만 VO와 Entity를 제대로 이해하고 개발하는데는 경험이 많이 필요하다.

Why use Entities

우리가 entity를 사용하는 이유.
domain 개념에서 다른 모든 객체외 반드시 구분해야 하는 경우, 즉 individuality가 필요할 때 entity로 설계한다.

entity는 긴 시간에 걸쳐 계속 변화할 수 있는 model.
처음 모습과 많이 달라질 수 있지만 같은 식별자를 가진다면 같은 객체다.

VO와 명확하게 구분되는 개념은 ID와 mutability.

  • 그치만 대부분의 domain model은 VO로 모델링 되는게 맞다.
  • Vaughn Vernon은 모든 model이 Entity로 모델링 될 바에는 Vo로 모델링 되는게 맞다고 했다. 그만큼 Vo로 최대한 모델링을 진행하라는 것.

ID

entity는 반드시 고유하게 식별되고 구분되어야 한다.
따라서 entity를 설계할 때는 같은 타입의 여러 객체 중에 원하는 entity를 찾을 때 필요한 속성이 무엇인지 아는게 중요하다.
그냥 모델링을 해놓고 ID를 붙이면 안된다는 말이다.

ID는 안정성 확보를 위해 immutable 해야 한다.
그렇기 때문에 entity의 ID로서 VO가 사용될 수 있다.
그리고 ID는 setter가 open 되지 않게 보호되어야 한다.

ID가 탐색이나 매칭에 사용될 수 있지만 그렇지 않을 가능성도 높다.

예시

인물 정보에 대한 검색을 예로들면 사람의 이름을 ID로 쓰는 일은 거의 없을 것이다.
당연히 중복된 이름이 존재할 수 있고 이는 entity의 특성에 어긋난다.
따라서 human readable하지 않은 ID가 내부적으로 존재할 수 있다.
위에서 설명한 원하는 entity를 찾을 때 필요한 속성이 여기서는 이름이 될 것이다.

ID 생성 방법

1. 사용자가 입력한 초기 고유 값을 사용

ID를 생성하는 일을 사용자에게 의지하겠다는 것.
이런 방식은 사실 양질의 ID를 만들 생각이 없다는 것.
ID를 사용자가 바꿔선 안되는데 이런 약속이 지켜져야 한다.

사실 이 방법은 쓰면 안되겠지.

2. application 내부에 고유성이 보장되는 알고리즘을 사용

library나 framework를 사용할 수도 있다.
UUID, GUID를 사용할 수 있다.

  • 신뢰도가 높고
  • 32bytes의 사람이 읽을 수 없는 값

이 방법이 가장 많이 쓰이는 방법일 것이다.

3. DB 같은 영속성 저장소를 사용해 ID 생성

db는 필요한 범위에 따라 고유 값을 생성해준다.

  • 도메인 모델이 db에 의존하게 된다.
  • ID를 얻기 위해 db까지 가야하니 성능상 단점이 될 수 있다.

대리 식별자

hibiernate 같은 경우는 숫자 시퀀스 같은 db primitive type을 식별자로 선호한다. 도메인에서 다른 유형의 식별자가 필요하다면 두 개의 식별자를 사용해야 한다.

  1. 도메인 모델에 맞춰 설계된 식별자
  2. hibernate를 위한 식별자, 대리 식별자

대리 속성은 도메인 모델의 일부가 아니기 때문에 감추는 편이 바람직하다. 대리 속성을 감추기 위해 abstract Entity class 같은 것을 만들어서 구현하도록 하는 방식으로 감출 수 있다.

4. 다른 바운디드 컨텍스트가 ID 할당

다른 바운디드 컨텍스트가 ID를 할당할 때는 ID의 검색과 매칭이 필요하다.

예시

ID를 찾기위해 계좌번호, 사용자 명, 이메일 주소 등의 속성을 제공해야 하고,
이 속성들과 ID가 1대 1로 매칭되는 것이 가장 이상적이다.

식별자 생성 시점

클라이언트가 도메인 이벤트를 구독하는 경우 새로운 entity의 인스턴스화가 완료되면 이벤트가 발생할 수 있다.
도메인 이벤트가 올바르게 초기화 되기 위해선 식별자 생성을 빠르게 완료해야 한다.

entity 설계

entity 설계 초기에는 ID를 중심으로 우선적인 특성과 행동을 비롯해 쿼리에 도움을 주는 요소에 집중한다.

  • entity를 다른 entity들과 구분할 수 있는 특성에 먼저 집중해서 ID를 확인하는 것.
  • 비지니스적 속성을 먼저 해결하려고하면 문제가 생길 수 있다.
  • ID와 엑세스 컨텍스트, entity를 찾기 위한 필수적인 속성을 먼저 고려해야 한다.

entity가 이름이나 설명과 같은 다른 수단으로도 query가 된다면 그 property들도 contructor에 포함시킨다.

  • 이렇게 생성이 성공적으로 이뤄지면 이 변수들은 절대 null이 되지 않는다. 이를 constructor와 constructor의 setter가 보장해야 한다.

예시

user 객체는 tenantId, username, password, person 등을 포함해야 한다.
person의 정보와 username으로 query가 가능해야 하기 때문에.

setter

setter를 설계할 수 있지만 요구사항의 용어를 제대로 표현하는 것이라고 볼 수 없다.
절대적으로 부적절한 것은 아니지만 하나의 요청을 완수하기 위해 여러 setter를 사용할 필요가 없을 때 사용한다.
다수의 setter는 의도를 모호하게 하기 때문이다.
또 이런 setter는 하나의 논리적인 커맨드가 되어야 하는 결과를 의미있는 도메인 이벤트로 게시하기 어렵게 만든다.

예시

setActive(boolean) -> activate(), deactivate() 로 변경.

activate 내부에서 active(boolean) 값의 변경과 updatedTime(time) 값의 변경이 동시에 이뤄질 수 있다.
이런 경우 setActive(), setUpdatedTime()을 호출하는건 안좋은 방향이다.

  • 위에서 말한 하나의 요청을 완수하기 위해 여러 setter를 사용해서 의도를 모호하게 하는 것. setActive() 내부에서 updatedTime을 수행하는 것도 안좋다.
  • setActive()라는 함수가 active만 set할 것 같은데 숨겨진 기능이 있는 것과 같음.

자가 캡슐화, Self Encapsulation

contructor에서 각 property의 할당을 setter에 위임하는 방식.
각 setter가 property의 validation을 책임지는 방식으로 각 변수에 대한 자가 캡슐을 지원한다.

  1. null이면 안된다.
  2. id인 경우 setter가 두번 호출될 수 없다. (set된 값이 null이 아니면 다시 호출하면 에러)

이런 검사를 하는 assertion들이 guard라고 불린다.

복잡한 constructor에 대해 property 별로 명확한 assertion을 제공하는 방향이 될 수 있는 것 같다.

유효성 검사

도메인 객체의 모든 특성이 개별적으로 유효하다고 해서 객체 전체가 하나의 대상으로 유효하다는 의미는 아니다.

  • test에서 mother1를 짜다보면 많이 느끼는 것.
  • 옳은 데이터끼리 짜집어도 결국 틀리는 부분이 있기 마련.

하나 이상의 단계로 이뤄진 유효성 검사를 통해 가능한 모든 문제를 다뤄야 한다.

  1. 객체의 인스턴스 변수를 추상화할 수 있도록 해준다.
  2. 이를 통해 해당 객체를 담고 있는 많은 다른 객체에서 손쉽게 특성/속성을 가져오는 방법을 제공한다.
  3. 유효성 검사의 단순한 형태를 지원한다.

property 유효성 검사

반 버논은 자가 캡슐화를 추천한다.
그치만 이걸 유효성 검사라고 하면 거부감이 든다고 한다.
유효성 검사는 도메인 객체가 아닌 유효성 검사 클래스의 책임이어야 하기 때문이다.
반 버논은 이걸 계약에 의한 설계 접근법 측면에서 assertion이라고 말한다.

entity의 단순한 특성도 entity의 일부인 것처럼, 이런 guard(자가 캡슐화된 setter)도 entity의 일부이다. 작은 특성들을 guard하지 않는다면 정신 나간 값이 설정되는 상황을 가드할 수 없다.

빈 문자열에 대한 확인은 동의하지만, 문자열 길이나 숫자의 범위 확인은 동의하지 않는 사람들이 있다.
이런 경우는 db에 확인을 맡기는게 최선이라고 생각하기도 한다.
문자열 길이가 모델과 관련이 없다고 생각하는 것인데, 이런 검사가 무결성 점검이라고 볼 수 있다.

전체 객체의 유효성 검사

전체 객체에서 유용한 방법은 마지막으로 확인 가능한 순간까지 확인을 지연시킨다.
유효성 검사에는 엔티티의 전체 상태가 사용 가능해야 하므로 유효성 검사 로직을 엔티티에 직접 넣으려고 할 수도 있다.
그치만 도메인 객체 자체보다 도메인 객체의 유효성 검사가 더 자주 변경된다 (번 바논 의견)
엔티티 내부에 유효성 검사를 넣으면 너무 많은 책임을 갖기도 한다.
이미 엔티티는 자신의 상태를 유지해 도메인 행동을 다뤄야 하는 책임을 갖고 있다.
엔티티는 유효성을 검사하는 방법을 알 필요는 없고 유효한지 결과만 알면된다.

유효성 검사 컴포넌트는 엔티티 상태가 유효한지 결정하는 책임을 갖는다.
자바에서는 엔티티랑 같은 패키지에 위치해서 package private으로 entity의 속성에 접근할 수 있도록 한다.

  • 유효성 검사를 위해 public이 되어야 하는 방향은 바람직하지 않다.

유효성 검사에서 첫 문제가 발생할 때 예외를 던지기 보단 절체 결과를 수집하는 것이 중요하다.

예시

Vaughn Vernon이 예를 든 validator 코드.

public class Warble extends Entity {

    @Override
    public void validate(ValidationNotificationHandler aHandler) {
        (new WarbleValidator(this, aHandler)).validate();
    }
}

class WarbleValidator extends Validator {
    public Validator (Warble aWarble, ValidationNotificationHandler aHandler) {
        super(aHandler);
        this.setWarble(aWarble);
    }

    public void validate() {
        this.checkForWarpedWarbleCondition();
        this.checkForWackyWarbleState();
    }
}

변화 추적

엔티티의 정의에 따라 수명주기에 걸쳐 일어나는 모든 상태 변경을 추적할 필요는 없다. 모델에서 일어나는 중요한 사건에 신경을 써야할 때가 있다. 이럴 때 엔티티에서 일어나는 특정 변경의 추적이 도움이 된다.

정확하고 유용하면서 실용적인 변경 추적은 도메인 이벤트와 이벤트 저장소를 사용하는 것이다.

reference

  • Implement Domain Driven Design (chapter5 Entity), Vaughn Vernon

  1. 테스트 객체를 생성하는 기술, ObjectMother 패턴 참고