[도메인 주도 설계 핵심] 5장 요약

애그리게잇과 전술적 설계

여기 2개의 바운디드 컨텍스트가 존재한다. 왼쪽은 애자일 프로젝트 관리 컨텍스트라는 이름의 핵심 도메인이고, 오른쪽은 컨텍스트 매핑 통합 기반의 협업 도구를 제공하는 지원 서브도메인이다. 

바운디드 컨텍스트 안에 자리잡은 개념들이 바로 모델 안에 존재하는 애그리게잇이다.


왜 필요할까?

두 바운디드 컨텍스트 안에 동그라미 쳐진 개념들 각각은 애그리게잇이다. 동그라미를 치지 않은 개념인 Discussion은 값 객체(Value Object)로 모델링한 것이다.

 

엔티티란 무엇일까?
엔티티는 독립적인 것이며, 각 엔티티는 같은 형태를 띠거나 다른 형태의 엔티티들과의 특성을 구별할 수 있는 고유한 식별성을 갖습니다.
엔티티는 변할 수 있으며, 변하지 않을수도 있다.

 

값 객체는 무엇인가?
값 객체는 불변의 개념적 완전성을 모델링한다. 모델에서 값은 엔티티와 달리 고유한 식별성이 없으며, 값 형태로 캡슐화된 속성을 비교함으로써 동일함이 결정된다.

애그리게잇은 1개 이상의 엔티티로 구성되고, 그 중 한 엔티티는 애그리게잇 루트라고 부른다. 애그리게잇은 그 구성에 값 객체를 포함할 수 있다.

각 애그리게잇의 루트 엔티티는 애그리게잇 안의 다른 모든 요소를 소유한다. 루트 엔티티의 명칭은 애그리게잇의 개념적 명칭이다. 

 

각 애그리게잇은 일관성 있는 트랜잭션 경계를 형성한다.

  ➡️ 트랜잭션 제어가 데이터베이스에 커밋될 때, 한 애그리게잇 내의 모든 요소는 반드시 비즈니스 규칙을 따르면서 일관성있게 처리된다는 것을 의미한다. (다만, 애그리게잇 내에 트랜잭션 이후 일관성이 지켜질 필요가 없는 다른 요소를 포함해서는 안된다는 뜻은 아니다.)

 

트랜잭션 경계를 두는 이유는 비즈니스 때문이다. 애그리게잇이 유효한 상태인지 아닌지를 결정하는 것은 비즈니스와 관련되어 있기 때문이다. 즉, 애그리게잇이 완전하고 유효한 상태로 저장되지 않는다면 수행된 비즈니스 오퍼레이션은 비즈니스 규칙에 어긋난 것이다.


애그리게잇 경험 법칙

애그리게잇 설계의 4가지 기본 규칙

1. 애그리게잇 경계 내에서 비즈니스 불변사항을 보호하라

2. 작은 애그리게잇을 설계하라

3. 오직 ID를 통해 다른 애그리게잇을 참고하라

4. 결과적 일관성을 사용해 다른 애그리게잇을 갱신하라

 

 

규칙1: 애그리게잇 경계 내의 비즈니스 불변 사항을 보호하라

트랜잭션이 커밋될 때 비즈니스 일관성이 지켜지는 것에 기반을 두고 애그리게잇 구성 요소를 결정해야 한다는 뜻이다.

 

위의 예제를 보면 Product는 트랜잭션의 끝에 ProductBacklogItem 인스턴스로 구성되는 모든 것이 반드시 Product의 루트와 일관되게 처리되도록 설계하고 있으며, Sprint도 트랜잭션의 끝에 CommittedBacklogItem 인스턴스로 구성되는 모든 것이 Sprint 루트와 일관되게 처리되도록 설계한다.

 

더 구체적인 사례를 통해 이해해보자

 

BacklogItem 애그리게잇은 "모든 Task 인스턴스의 hoursRemaining이 0일 때, BacklogItem의 status는 반드시 DONE으로 설정해야 한다"라는 비즈니스 규칙이 있다고 가정하자. 이는 트랜잭션 후에 반드시 이뤄져야 하는 명확한 비즈니스 불변사항이자 요청사항이다.

 

규칙2: 작은 애그리게잇을 설계하라

각 애그리게잇의 메모리 사용량과 트랜잭션 범위가 비교적 작아야 함을 강조한다.

 

위의 애그리게잇은 작지 않은 편이다. Product는 BacklogItem, Sprint, Release을 담고 있으며 시간이 지나면 Product의 책임은 점점 커지게 된다. 

 

Product를 4개의 애그리게잇으로 구분하면, Product, BacklogItem, Sprint, Release 애그리게잇으로 분리할 수 있다. 이렇게 분리함으로써 얻는 장점은 크게 2개이다. 첫 번째는 애그리게잇들이 빠르게 로드되고, 더 작은 메모리를 차지할 수 있다는 것이고, 두 번째는 애그리게잇들은 이전의 큰 클러스터의 Product 애그리게잇보다 훨씬 더 자주, 성공적인 트랜잭션을 수행하게 된다는 것이다.

 

규칙3: 오직 식별자로만 다른 애그리게잇을 참고하라

이 각각의 애그리게잇들은 필요한 다른 애그리게잇들을 참고할 때, 식별자를 사용한다. 

이 사례에서 BacklogItem, Release, Sprint 모두가 ProductId를 유지함으로써 Product를 참고하는 것을 확인할 수 있다.

이것은 애그리게잇을 작게 유지하고, 동일한 트랜잭션 내에 여러 애그리게잇을 수정하려는 접근을 방지해준다.

 

이 규칙을 통해 동일한 트랜잭션 내에 다른 애그리게잇을 수정하지 않는 규칙이 잘 지켜지도록 도와주며, 오직 애그리게잇의 식별자를 통해서만 접근이 가능하고 그 외의 방법으로 다른 애그리게잇 내의 객체 레퍼런스를 얻어낼 수 있는 방법은 없다.

 

규칙4: 결과적 일관성을 사용해 다른 애그리게잇을 갱신하라

BacklogItem은 Sprint와 연계되어 수행된다. 먼저 BacklogItem은 관여된 Sprint를 알아야 한다. 이는 BacklogItem의 상태가 해당 Sprint의 SprintId를 갖도록 정의하는 하나의 트랜잭션 안에서 관리된다. 

 

그러면 Sprint 관점에서 새롭게 할당된 BacklogItem의 BacklogItemId로 연계되어 제대로 실행되었는지 어떻게 확신할 수 있을까?

BacklogItem 애그리게잇의 트랜잭션의 일부로, BacklogItemCommitted라는 도메인 이벤트를 발행시킨다. BacklogItem 트랜잭션 완료 후의 상태는 BacklogItemCommitted 도메인 이벤트를 통해 유지된다. BacklogItemCommitted가 로컬 구독자(Sprint 애그리게잇)에게 전달되면 트랜잭션이 시작되고, Sprint의 상태는 할당된 BacklogItem의 BacklogItemId를 보유하도록 수정된다.

그리고, Sprint는 새로운 CommittedBacklogItem 엔티티 안에 BacklogItemId를 보유한다.

 

도메인 이벤트는 애그리게잇에 의해 발행되고, 이에 관심이 있는 바운디드 컨텍스트는 이를 전달받는다. 이처럼 메시징 메커니즘은 구독을 통해 관심 있는 파티들에게 도메인 이벤트를 전달한다.

 


애그리게잇 모델링

 

주의해야할 점

1. 빈약한 도메인 모델(Anemic Domain Model)을 만들지 않도록 해야 한다.

2. 비즈니스 로직이 도메인 모델을 넘어 애플리케이션 서비스까지 새어나가지 않도록 주의해야 한다. 

 

도메인 모델링하기

 

추상화를 조심스럽게 선택하라

효과적인 소프트웨어 모델은 항상 비즈니스의 방식을 고려한 일련의 추상화에 기반을 두고 있다. 이 때, 모델링하는 각 개념마다 적절한 수준의 추상화를 선택해야 한다. 하지만 가끔은 잘못된 문제를 푸는 것에 지나치게 몰두한 나머지 소프트웨어 개발자가 지나칠 정도로 추상화를 적용하기도 한다.

 

소프트웨어 개발자가 스크럼의 보편언어를 모델링하는 것에 그다지 관심을 기울이지 않은 채, 현재와 미래의 모든 스크럼 관련 개념을 모델링하는 것에 관심을 가지게 된다면 여러 문제점들이 생기게 된다.

 

1. 소프트웨어 모델의 언어가 도메인 전문가의 멘탈 모델과 일치하지 않는다.

2. 추상화 수준이 너무 높아서 각 개별적인 형태의 세부사항을 모델링하면 어려운 상황에 빠진다.

3. 잘못된 추상화 수준은 많은 시간을 낭비하게 한다. 

4. 프로젝트 초반에 미래의 모든 요구를 생각하고 반영할 수는 없다. 새로운 스크럼 개념들은 앞으로도 계속 추가될 것이고, 기존 모델은 그 요구사항을 예견하는데 실패할 수 밖에 없기 때문이다.