3. 애그리거트

애그리거트

 

객체 상위 수준 관점

 

객체 개별 수준 관점

 

위 그림 중 어떤 그림이 전체적인 구조를 파악할 때 더 쉬워 보이는가? 당연히 나무 보다 숲을 먼저 보듯이 상위 수준의 첫번째 그림이 더 쉬워보인다.

 

복잡한 도메인을 이해하고 관리하기 쉬운 단위로 만들려면 상위 수준에서 모델을 조망할 수 있는 방법이 필요한데, 그 방법이 바로 애그리거트이다. 수많은 객체를 애그리거트로 묶어서 바라보면 좀 더 상위 수준에서 도메인 모델 간의 관계를 파악할 수 있다.

 

다음 그림은 애그리거트로 다시 묶어서 표현한 것이다.

 

 

애그리거트는 복잡한 도메인을 단순한 구조로 만들어준다. 복잡도가 낮아지는 만큼 도메인 기능을 확장하고 변경하는데 필요한 노력도 줄어든다.

 

위 그림처럼 애그리거트는 경계를 가지며 한 애그리거트에 속한 객체는 다른 애그리거트에 속하지 않는다. 즉, 각 애그리거트는 자기 자신을 관리할 다른 애그리거트를 관리하지 않는다.

 

 

애그리거트 루트

 

애그리거트에 속한 모든 객체는 일관된 상태를 유지해야한다. 

예를 들어 주문 애그리거트는 다음을 포함한다고 가정하자.

  • 총 금액인 totalAmounts를 갖고 있는 Order 엔티티(OrderLine 밸류를 가지고있음)
  • 개별 구매 상품의 개수 quantity, 금액 price를 갖고 있는 OrderLine 밸류

위의 가정을 베이스로 구매할 상품의 개수를 변경하면 한 OrderLine의 quantity를 변경하고 더불어 Order의 totalAmounts도 변경해야 한다. 그렇지 않으면 '주문 총 금액은 개별 상품의 주문 개수 X 가격의 합' 라는 도메인 규칙을 어기고 객체들 상태의 일관성이 깨진다.

 

이 책임을 지는 것이 바로 애그리거트의 루트 엔티티이다. 애그리거트 루트 엔티티는 애그리거트의 대표 엔티티로 애그리거트에 속한 객체는 에그리거트 루트 엔티티에 직접 또는 간접적으로 속한다.

 

 

도메인 규칙과 일관성

 

애그리거트 루트가 아닌 다른 객체가 애그리거트에 속한 객체를 직접 변경하면 안된다. 이는 애그리거트 루트가 강제하는 규칙을 적용할 수 없어 모델의 일관성이 깨지는 원인이 된다.

이를 예방하기 위해 다음과 같은 두 가지를 습관적으로 적용해야 한다.

  • 단순히 필드를 변경하는 setter를 만들지 않는다. (외부에서 내부 상태를 함부로 바꾸지 못하도록 차단한다.)
  • 밸류 타입은 불변으로 구현한다. (애그리거트 루트가 제공하는 메서드를 통해 새로운 밸류 객체로 교체하는 방법만 제공한다.)

 

 

트랜잭션 범위

 

한 트랜잭션에서 한 애그리거트를 변경하는 것을 권장하지만 한 애그리거트에서 다른 애그리거트를 수정하면 결과적으로 두 개의 애그리거트의 소통이 한 트랜잭션안에 존재하며 한 애그리거트 내부에서 다른 애그리거트의 상태를 변경하는 꼴이 되어버린다. 애그리거트는 자신의 책임 범위를 넘어 다른 애그리거트의 상태까지 관리하면 안된다. (애그리거트 간 결합도가 높아짐)

 

부득이하게 한 트랜잭션으로 두 개 이상의 애그리거트를 수정해야 한다면 응용 서비스 계층에서 두 애그리거트를 수정하도록 구현하면 된다.

 

 

리포지토리와 애그리거트

 

애그리거트는 개념상 완전한 한 개의 도메인 모델을 표현하므로 객체의 영속성을 처리하는 리포지터리는 애그리거트 단위로 존재한다.

(Order, OrderLine가 존재할 때 루트 엔티티인 Order의 리포지터리만 만든다.)

 

그리고 애그리거트를 구하는 리포지터리 메서드는 완전한 애그리거트를 제공해야한다.

 

Order order = orderRepository.findById(orderId);

 

위의 코드를 실행하면 order 애그리거트는 OrderLine, Orderer 등 모든 구성요소를 포함해야 한다. 그렇지 않으면 완전하지 않은 애그리거트이므로 NPE같은 문제가 발생되기 때문이다.

 

 

ID를 이용한 애그리거트 참조

 

한 객체가 다른 객체를 참조하는 것처럼 애그리거트도 다른 애그리거트를 참조한다. 애그리거트의 관리 주체가 애그리거트 루트이므로 애그리거트에서 다른 애그리거트를 참조한다는 것은 애그리거트의 루트를 참조한다는 것과 같다.

 

 

합성(필드를 통해 다른 애그리거트를 직접 참조)하면 다음과 같이 order를 통해 회원 아이디를 쉽게 구할 수 있다.

 

order.getOrderer().getMember().getId()

 

하지만, 합성을 이용한 애그리거트 참조는 다음의 문제를 야기한다.

  • 편한 탐색 오용
  • 성능에 대한 고민
  • 확장 어려움

 

편한 탐색 오용

 

이전의 트랜잭션 범위에서 언급했던것 처럼 한 애그리거트에서 다른 애그리거트의 상태를 변경하는 것은 애그리거트간의 의존 결합도를 높여 결과적으로 애그리거트의 변경을 어렵게 만든다.

 

 

성능에 대한 고민

 

예를 들어 JPA를 사용할 경우 참조한 객체에 대해 지연로딩 또는 즉시로딩을 사용할 수 있는데 다양한 경우의 수를 고려해서 성능상 효율적인 로딩전략을 결정해야한다.

 

 

확장의 어려움

 

사용자가 늘고 트래픽이 증가하면 이에따라 부하를 분산시키기 위해 도메인별로 시스템을 분리시킬것이다. 만약 도메인 마다 다른 종류의 데이터 저장소를 사용한다면 더 이상 다른 애그리거트 루트를 참조하기 위해 JPA와 같은 단일 기술을 사용할 수 없음을 의미한다.

 

💡 위의 세가지 문제점을 완화시키기 위해서는 다음 그림과 같이 ID를 이용해서 다른 애그리거트를 참조해야한다.

 

 

 

편한 탐색 오용

 

애그리거트를 참조하는것이 아닌 ID 참조하는것이기 때문에 애그리거트간 의존을 제거하며 경계를 명확히 해준다. (한 애그리거트에서 다른애그리거트를 수정하는 문제를 원천적으로 방지할 수 있다.)

 

 

성능에 대한 고민

 

응용 서비스에서 ID를 통해 필요한 애그리거트를 로딩하므로 로딩전략에 대해 고민하지 않아도 되며 지연 로딩을 하는것과 동일한 결과를 낫는다.

 

 

확장의 어려움

 

 

애그리거트별로 다른 구현 기술을 사용하는 것이 가능해진다.

 

 

ID를 이용한 참조와 조회 성능

 

ID를 이용해서 참조한 방식을 사용하면 지연로딩과 같은 효과를 내는데 이는 N+1 조회 문제를 발생시킨다.

이를 해결하기 위해서는 세타조인을 이용해서 한 번의 쿼리로 필요한 데이터를 로딩시키는 전용 조회 쿼리를 작성하면된다. (5장에서 더 자세히 설명)

 

 

 

'📚 Book > DDD Start!' 카테고리의 다른 글

7. 도메인 서비스  (0) 2020.08.12
6. 응용서비스와 표현영역  (0) 2020.08.09
2. 아키텍처 개요  (2) 2020.07.12
1. 도메인 모델 시작  (0) 2020.07.01