DDD(Domain Driven Design) - Aggregate (어그리게잇)

DDD 에서 중요한 개념인 Entity 와 Value Object 에 대해서 다른 글들에서 알아봤다. 하지만 이외에도 가장 중요한 개념이 아마 Aggregate 가 아닌가 생각한다.

그렇다면 Aggregate ( 애그리게잇 ) 이란 과연 무엇인가 ??!

어그리게잇은 Entity 와 Value Object 들의 집합이며, 함께 클러스터링 되어 트랜잭션의 경계를 이룬다.
어그리게잇자체도 Entity 이기에 mutability 를 가지고 있다.
또한 Invariants 를 가진다. 비즈니스 상으로 필수적으로 일관성을 지켜야하는 룰이 있다. 예를 들어 Order 라는 주문 애그리게잇은 주문하려는 상품의 재고가 0일 경우 주문이 이루어 질 수 없다. 이는 Order 라는 애그리게잇의 생성과 관련된 필수적으로 일관성을 지켜야하는 룰이다.


Aggregate 를 설계할 때 주의해야 할 점을 다루어보겠다.

크기가 큰 Aggregate 는 좋은가?


-> 좋지 않다. Aggregate 는 Entity 이면서 동시에 다른 Entity 를 가질 수 있다. 이는 생애주기를 가지는 객체라는 말을 의미하게 되는데, 크키가 커질수록 당연히 클러스터링 되는 Entity 또한 증가할 것이다.
따라서 Entity 가 커질수록 트랜잭션을 다루기 어려워지며, 트래픽이 증가할 수록 동시성 이슈도 고려를 해야한다. 동시성 이슈는 DDD 에서는 Version 을 통해 Optimistic Locking 기법을 보통 사용한다. Pessimistic Locking 의 경우 Database level 에서 컨트롤하기 때문에 이는 Domain 에 기반한 접근과 거리가 있다.


진짜 고정자를 일관성 경계 안에 모델링하라

-> 고정자는 앞서 언급했듯이, 언제나 일관성을 유지해야만 하는 비즈니스 규칙이다.
일관성에는 아래와 같이 두 가지를 다룰 수 있다.
  1. 트랜잭션적 일관성 ( Transactional consistency )
  2. 결과적 일관성 ( eventual consistency )
고정자와 관련된 일관성은 트랜잭션적 일관성과 관련이 있다.

아래 예시에서, a 가 2 이고 b 가 3일 때 c 는 반드시 5여야 한다. 이 규칙과 조건이라면, c 가 5가 아닌 모든 경우에 시스템 고정자를 위반하게 된다. 
 c 가 일관성을 갖게 하기 위해, 아래와 같이 모델의 특성을 둘러싸는 경계를 설계한다.
일관성 경계는 어떤 operation 이 수행되든 상관없이 경계 안의 모든 대상이 특정 비즈니스 고정자 규칙 집합을 준수하도록 논리적으로 보장해준다.

전형적인 영속성 메커니즘을 사용하면 단일 트랜잭션 ( Unit of work ) 을 사용해 일관성을 관리한다. 트랜잭션이 커밋하는 시점에서 하나의 경계 안에 속한 모든 것이 반드시 일관성을 유지해야 한다.
올바르게 설계한 Aggregate 은 단일 트랜잭션 내에서 완벽한 일관성을 유지하면서, 비즈니스적으로 요구되는 모든 방식과 그 고정자에 맞춰 수정 될 수 있어야 한다.

또한 바르게 설계된 바운디드 컨텍스트는 어떤 상황에서든 한 트랜잭션당 한 애그리게잇 인스턴스만을 수정한다. 따라서 트랜잭션 분석을 진행하지 않으면 애그리게잇 설계를 올바르게 할 수 없다.


그렇다면, Aggregate 을 설계할 때, 크지 않게, 작게, 필요한 만큼은 얼마 만큼인가?


Aggreate 안의 한 부분을 Entity 로 설계한다고 생각해보자. 
  • 해당 부분이 시간에 따라 변화 하는가?
  • 아니면, 변경이 필요할 때 완전히 대체 (replace) 시키는가?
만약, 후자라면 이는 Entity 가 아니라 Value Object 로 설계하는것이 맞다. 만약 이 기준으로 Aggregate 을 보게 되면, 많은 Entity 들이 Value Object 로 리팩토링 되어야 함을 알 수 있다.

Entity 가 Value Object 도 리팩토링 된다는 말에서 감이 왔을 것이다. 이는 Mutability 를 제한하기 때문에, 트랜잭션의 경계를 줄일 수 있다. 

책에서는 한 예시를 들고 있다. 파생 금융 상품 부문 관련 프로젝트에서, 약 70 퍼센트의 Aggregate 을 값 타입 속성을 포함한 단하나의 루트 Entity 만으로 설계할 수 있다는 사례를 들고 있다. 남은 30 퍼센트에선 총 두개나 세 개 정도의 Entity 가 필요했다고 한다.

따라서 높은 비율로 Aggregate 이 루트는 하나의 Entity 만으로 구성할 수 있다.

크키가 작은 애그리게잇은 성능과 확장성이 더 좋을 뿐 아니라, 커밋을 가로막는 문제가 거의 일어나지 않기 때문에 트랜잭션이 성공할 가능성이 높다.


ID 로 다른 Aggregate 을 참조하라.

Aggregate 이 다른 Aggregate root 로의 참조를 가질 수 있다고 Eric Evans 형님께서 말씀하셨는데, 이는 참조된 Aggregate 을 참조하고 있는 Aggregate 의 일관성 경계 안쪽으로 위치시킨다는 의미가 아니라는 점을 기억해야 한다.

아래에는 잘못된 예시를 볼 수 있다. Java 에서 Association 을 나타낸다.

왜 잘못되었는지 따져보자.
참조하는 Aggregate (BacklogItem) 과 참조된 Aggregate (Product) 을 같은 트랜잭션 안에서 수정해선 안 된다. 하나의 트랜잭션에선 하나의 Aggregate 만 수정해야 한다.
하나의 트랜잭션에서 여러 인스턴스를 수정하고 있다면 일관성 경계가 잘못됐다는 신호일 가능성이 높다.

그렇다면, 어떻게 설계하는것이 맞을까? 직접 객체 참조를 하는 것은 옳지 않다. 
아래와 같이 전역 고유 식별자 Value Object 를 이용하면 된다. 이는 추론 객체 참조 (inferred object reference) 라고도 한다. BacklogItem 은 즉시 Product 참조할 필요가 없기 때문에, 당연히 크기가 작아진다. 인스턴스를 가져올 때 더 짧은 시간과 적은 메모리가 필요하기 때문에, 성능도 좋아진다.



이는 단절된 도메인 모델 (Disconnected Domain Model) 이란 기법이다. 실제론 지연 로딩 (Lazy loading) 의 한 형태이다.


경계의 밖에선 결과적 일관성을 사용하라.

"애그리게잇을 아우르는 규칙이 언제나 최신 상태로 유지되길 기대할 순 없다. 이벤트 처리, 배치 처리, 그 밖의 업데이트 메커니즘을 통해 지정된 시간 내에서 의존성이 해결될 수 있도록 할 수 있다."

하나의 Aggregate 인스턴스에서 커맨드를 수행할 때 하나 이상의 Aggregate 에서 추가적인 비즈니 규칙이 수행되어야 한다면 결과적 일관성 (Eventual Consistency)을 사용하자.

도메인 이벤트를 발행하고 이벤트를 구독하는 발행-구독 아키텍쳐를 생각해보자. 이는 분리된 트랜잭션 내에서 수행되지만, 단일 트랜잭션(분산 트랜잭션이지만)에서 하나의 Aggregate 인스턴스 만을 수정한다는 규칙을 따른다.

그렇다면 Transaction Consistency Vs Eventual Consistency ?
Eric Evans 형님께서 올바른 지침을 알려주셨다. 
유스케이스를 논의 할 때 데이터의 일관성을 보장하는 주체가 유스케이스를 수행하는 사용자의 일인지를 질문해보자. 만약 그렇다면, 다른 Aggregate 의 규칙들은 고수하는 가운데 트랜잭션(Transactional Consistency)을 통해 일관성을 보장하도록 하자. 만약 다른 사용자나 시스템이 해야할 일이라면 결과적 일관성(Eventual Consistency)을 선택하자.


'데메테르의 법칙' 과 '묻지 말고 시켜라'

Aggregate 구현할  때 사용할 수 있는 설계 원칙이다. 둘 모두 정보 은닉을 강조하고 있다.

데메테르의 법칙 : 최소 지식의 원칙 ( principal of least knowledge ) 을 강조한다. 예를 들어, 두 객체가 존재하고 한 객체를 클라이언트 객체라고 한다. 그리고 클라이언트 객체는 시스템 행동을 실행하기 위해 이용하는 나머지 객체를 서버 객체라고 부르자. 이 상황에서, 클라이언트 객체는 서버 객체를 사용할 때는 서버의 구조에 관해 가능한 모르는 편이 좋다. 서버의 특성과 속성은 클라이언트에게 완벽히 감춰져야 한다. 클라이언트는 서버에게 표면 인터페이스상에 선언된 커맨드를 수행토록 부탁할 수 있다. 그러나 클라이언트는 서버 안쪽까지 도달해선 안 되고, 서버에게 일부 내부 파트를 부탁한 후 해당 파트상의 커맨드를 실행해야 한다. 만약 클라이언트가 서버의 내부 파트에 의해 렌더링된 서비스가 필요하다면, 클라이언트에겐 그 행동을 요청하기 위한 내부 파트로의 액세스가 주어져선 안 된다. 대신 서버는 표면 인터페이스를 제공하고, 호출이 이뤄지면 적절한 내부 파트로 위임해 해당 인터페이스를 완수토록 해야 한다.

-> 모든 객체의 모든 메소드는 다음을 통해서만 메소드를 호출해야 한다.
  1. 그 자신
  2. 자신에게 전달된 매개변수
  3. 자신이 인스턴스화하는 객체
  4. 자신이 직접 액세스할 수 있는 스스로가 포함된 파트 객체 

묻지 말고 시켜라 : 단순히 객체에게 할 일을 알려줘야 한다는 점을 강조한다. '묻지 말고' 란 말은 다음과 같이 클라이언트에게 적용된다. 클라이언트 객체는 서버 객체에게 서버 객체가 갖고 있는 파트를 요구해선 안되며, 자신이 갖고 있는 상태에 기반해 결정해야 하고, 그 후에 서버 객체가 일을 하도록 만들어야 한다. 그 대신, 클라이언트는 반드시 서버에게 무엇을 할 지 '시켜야' 하며, 이때는 서버의 퍼블릭 인터페이스로 커맨드를 보내야 한다. 데메테르 법칙과 매우 유사하지만 더 넓은 범위에 쉽게 적용이 가능하다.


낙관적 동시성

version 특성을 어디에 위치시켜야 하는지 생각해봐야 한다. Aggregate 의 정의를 생각해보면, 버전은 오직 루트 엔터티에서만 관리하는 편이 좋아 보인다. 루트 엔터티에 속한 모든 하위 엔터티들의 버전을 관리하는 것이 옳지 않을  때도 있다. 루트 버전의 변경이 고정자를 보호하는 유일한 방법이 될 때도 있다.

의존성 주입을 피하라

일반적으로 리파지토리나 도메인 서비스의 Aggregate 으로의 의존성 주입은 나쁘다고 볼 수 있다. 해당 상황이 일어날 수 있는 이유는 Aggregate 내부에서 의존적 객체 인스턴스를 찾고 싶기 때문일 것이다. 해당 객체가 다른 애그리게잇과 의존성을 갖고 있을 수 있다. 

따라서, 다른 Aggregate 을 참조할 땐, ID 로 해당 Aggregate 커맨드 메소드가 호출되기 전에 찾아서 전달하는 편이 좋다. 하지만 앞서 언급했듯이, 단절된 도메인 모델(Disconnected Domain Model) 은 일반적으로 덜 선호된다. 트래픽이 증가할 수록 또한 높은 성능을 발휘해야 하는 도메인이면 리파지토리와 도메인서비스 인스턴스를 Aggregate 으로 주입하는 데 따른 잠재적 오버헤드를 생각해보자.

댓글

이 블로그의 인기 게시물

About JVM Warm up

About idempotent

About Kafka Basic

About ZGC

sneak peek jitpack

Spring Boot Actuator readiness, liveness probes on k8s

About Websocket minimize data size and data transfer cost on cloud

About G1 GC

대학생 코딩 과제 대행 java, python, oracle 네 번째