해당 포스팅은 최범균 작가님의 도메인 주도 개발 시작하기 (P.98~107)를 읽고 정리한 글입니다.
애그리거트
도메인 객체 모델이 복잡해지면 개별 구성요소 위주로 모델을 이해하게 되고 전반적인 구조나 큰 수준에서 도메인 간의 관계를 파악하기 어려워진다. 관계를 파악하기 어렵다는 것은 코드를 변경하고 확장하는 것이 어려워진다는 것을 의미한다.
복잡한 도메인을 이해하고 관리하기 쉬운 단위로 만들려면 상위 수준에서 모델을 조망할 수 있는 방법이 필요한데, 그 방법이 바로 애그리거트다. 애그리거트는 관련된 객체를 하나의 군으로 묶어준다.
애그리거트는 모델을 이해하는데 도움을 줄 뿐만 아니라 일관성을 관리하는 기준도 된다. 모델을 보다 잘 이해할 수 있고 애그리거트 단위로 일관성을 관리하기 때문에, 애그리거트는 복잡한 도메인을 단순한 구조로 만들어준다. 복잡도가 낮아지는 만큼 도메인 기능을 확장하고 변경하는 데 필요한 노력(개발 시간)도 줄어든다.
애그리거트는 관련된 모델을 하나로 모았기 때문에 한 애그리거트에 속한 객체는 유사하거나 동일한 라이프 사이클을 갖는다. 도메인 규칙에 따라 어떤 시점에서 일부 객체를 만들 필요가 없는 경우도 있지만 애그리거트에 속한 구성요소는 대부분 함께 생성하고 함께 제거한다.
애그리거트는 경계를 갖는다. 한 애그리거트에 속한 객체는 다른 애그리거트에 속하지 않는다. 경계를 설정할 때 기본이 되는 것은 도메인 규칙과 요구사항이다. 도메인 규칙에 따라 함께 생성되는 구성요소는 한 애그리거트에 속할 가능성이 높다.
흔히 'A가 B를 갖는다'로 설계할 수 있는 요구사항이 있다면 A와 B를 한 애그리거트로 묶어서 생각하기 쉽다. 주문에 경우 Order가 ShippingInfo와 Orderer를 가지므로 이는 어느 정도 타당하다. 하지만 'A가 B를 갖는다'로 해석할 수 있는 요구사항이 있다고 하더라도 이것이 반드시 A와 B가 한 애그리거트에 속한다는 것을 의미하는 것은 아니다.
예를 들어 상품과 리뷰가 있다. 상품 상세 페이지에 들어가면 상품 상세 정보와 함께 리뷰 내용을 보여줘야 한다는 요구사항이 있을 때 Product 엔티티와 Review 엔티티가 한 애그리거트에 속한다고 생각할 수 있다. 하지만 Product와 Review는 함께 생성되지 않고, 함께 변경되지도 않는다. 게다가 Product를 변경하는 주체가 상품 담당자라면 Review를 생성하고 변경하는 주체는 고객이다.
애그리거트 루트
주문 애그리거트는 다음을 포함한다.
- 총 금액인 totalAmounts를 갖고 있는 Order 엔티티
- 개별 구매 상품의 개수인 quantity와 금액인 price를 갖고 있는 OrderLine 밸류
구매할 상품의 개수를 변경하면 한 OrderLine의 quantity를 변경하고 더불어 Order의 totalAmounts도 변경해야 한다. 그렇지 않으면 다음 도메인 규칙을 어기고 데이터 일관성이 깨진다. 애그리거트는 여러 객체로 구성되기 때문에 한 객체만 상태가 정상이면 안된다.
애그리거트에 속한 모든 객체가 일관된 상태를 유지하려면 애그리거트 전체를 관리할 주체가 필요한데, 이 책임을 지는 것이 바로 애그리거트 루트 엔티티이다. 애그리거트 루트 엔티티는 애그리거트의 대표 엔티티다.
도메인 규칙과 일관성
애그리거트 루트의 핵심 역할은 애그리거트의 일관성이 깨지지 않도록 하는 것이다. 이를 위해 애그리거트 루트는 애그리거트가 제공해야 할 도메인 기능을 구현한다. 애그리거트 루트가 제공하는 메서드는 도메인 규칙에 따라 애그리거트에 속한 객체의 일관성이 깨지지 않도록 구현해야 한다.
public class Order {
// 애그리거트 루트는 도메인 규칙을 구현하는 기능을 제공한다.
public void changeShippingInfo(ShippingInfo newShippingInfo){
verifyNotYetShipped();
setShippingInfo(newShippingInfo);
}
private void verifyNotYetShipped(){
if(state != OrderState.PAYMENT_WAITING && state != OrderState.PREPARING)
throw new IllegalStateException("already shipped");
}
}
}
애그리거트 외부에서 애그리거트에 속한 객체를 직접 변경하면 안 된다. 이것은 애그리거트 루트가 강제하는 규칙을 적용할 수 없어 모델의 일관성을 깨는 원인이 된다.
ShippingInfo si = order.getShippingInfo();
si.setAddress(newAddress);
이 코드는 애그리거트 루트인 Order에서 ShippingInfo를 가져와 직접 정보를 변경하고 있다. 이는 업무 규칙을 무시하고 직접 DB 테이블의 데이터를 수정하는 것과 같은 결과를 만든다. 즉 논리적인 데이터 일관성이 깨지게 되는 것이다. 일관성을 지키기 위해 상태 확인 로직을 응용 서비스에 구현할 수도 있다. 하지만 이렇게 되면 동일한 검사 로직을 여러 응용 서비스에서 중복으로 구현할 가능성이 높아져 유지 보수에 도움이 되지 않는다.
불필요한 중복을 피하고 애그리거트 루트를 통해서만 도메인 로직을 구현하게 만들려면 도메인 모델에 대해 다음 두 가지를 습관적으로 적용해야 한다.
- 단순히 필드를 변경하는 set 메서드를 공개(public) 범위로 만들지 않는다.
- 밸류 타입은 불변으로 구현한다.
공개 set 메서드는 도메인의 의미나 의도를 표현하지 못하고 도메인 로직을 도메인 객체가 아닌 응용 영역이나 표현 영역으로 분산시킨다. 도메인 로직이 한 곳에 응집되지 않으므로 코드를 유지 보수할 때에는 분석하고 수정하는데 더 많은 시간이 필요하다.
애그리거트에 대해 개념적으로 완벽히 알지는 못했지만 프로젝트를 수행하면서 어느 정도 적용은 한 거 같다. 예전 DDD 관련 강의를 들을 때 애그리거트 루트가 왜 필요한지에 대한 의문이 생겼는데 이번 독서를 통해 완벽히 이해한 거 같다. 애그리거트 내의 데이터의 일관성을 지키기 위함이라는 것을 확실히 머릿속에 저장했다.
'📚 개발자의 서재 > 도메인 주도 개발 시작하기' 카테고리의 다른 글
[DDD] 애그리거트 -3 (0) | 2025.04.28 |
---|---|
[DDD] 애그리거트 -2 (0) | 2025.04.27 |
[DDD] 아키텍처 개요 -5 (0) | 2025.04.25 |
[DDD] 아키텍처 개요 -4 (0) | 2025.04.24 |
[DDD] 아키텍처 개요 -3 (0) | 2025.04.24 |