에릭 에반스는 애그리거트를 "데이터 변경의 단위로 취급되는 연관 객체의 묶음"이라고 정의했다.
내가 읽고 있는 『도메인 주도 개발 시작하기』에서는 이를 조금 더 이해하기 쉽게 설명한다.
복잡한 도메인을 이해하고 관리하기 쉬운 단위로 만들기 위해 상위 수준에서 조망할 수 있는 모델
결국 애그리거트는 관련 있는 객체들을 적절한 경계 안으로 묶어서 복잡도를 낮추는 역할을 한다.
덕분에 도메인을 바라보기가 쉬워지고, 기능을 수정하거나 확장할 때도 영향 범위를 예측하기가 훨씬 수월해진다.
애그리거트의 특징을 정리해 보면 다음과 같다.
애그리거트의 경계를 정하는 기준은 객체 간의 연관관계가 아니라 도메인 규칙과 요구사항이다.
경계를 정하는 과정에서 자주 당하는 함정이 있다.
"A가 B를 가진다"는 사실만으로 A와 B가 같은 애그리거트인 것은 아니다.
처음에는 포함 관계가 있으면 같은 애그리거트라고 생각하기 쉬운데, 실제 기준은 함께 생성되고 함께 변경되는지 여부다.
즉,
이런 질문에 "그렇다"라고 답할 수 있다면 같은 애그리거트일 가능성이 높다.
애그리거트 안에는 여러 객체가 존재한다.
하지만 이 객체들이 마음대로 상태를 변경하면 애그리거트의 일관성은 쉽게 깨질 수 있다.
그래서 애그리거트에는 모든 변경을 책임지는 대표 객체, 즉 애그리거트 루트(Aggregate Root)가 존재한다.
애그리거트 루트의 가장 중요한 역할은 단 하나다.
애그리거트의 일관성을 지키는 것
이를 위해 루트는 애그리거트가 제공해야 하는 모든 도메인 기능을 구현한다.
그래서 DDD에서는 몇 가지 원칙을 강조한다.
setXXX()처럼 상태를 무분별하게 변경하는 public Setter를 만들지 않는다.결국 상태 변경은 항상 애그리거트 루트를 통해서만 이루어져야 한다.
DDD에서 가장 많이 강조되는 원칙 중 하나가 있다.
한 트랜잭션에서는 하나의 애그리거트만 수정한다.
여러 애그리거트를 동시에 수정하기 시작하면 결합도가 높아지고, 일관성을 유지하기 어려워진다.
만약 두 개 이상의 애그리거트를 함께 수정해야 한다면,
애그리거트가 서로를 직접 수정하는 것이 아니라 응용 서비스(Application Service)가 각각의 애그리거트를 호출해서 변경하도록 구현하는 것이 권장된다.
리포지터리 역시 엔티티 하나가 아니라 애그리거트 단위로 존재한다.
즉,
결국 리포지터리는 애그리거트의 생명주기를 관리하는 역할이라고 이해하면 될 것 같다.
애그리거트도 다른 애그리거트와 관계를 맺는다.
하지만 DDD에서는 객체 자체를 참조하기보다 ID를 통해 참조하는 방식을 권장한다.
처음에는 조금 불편해 보였지만 이유를 알고 나니 충분히 납득이 갔다.
객체를 직접 참조하면 여러 문제가 생길 수 있다.
객체가 바로 연결되어 있으면 의도하지 않게 다른 애그리거트의 상태를 변경하기 쉬워진다.
결국 애그리거트의 경계가 무너지게 된다.
직접 참조하면 Lazy Loading과 Eager Loading을 상황마다 고민해야 한다.
ID만 가지고 있다면 필요한 시점에만 조회하면 되므로 이런 고민이 줄어든다.
서비스가 커지면 도메인별로 DB를 분리하거나 마이크로서비스로 나누는 경우가 생긴다.
이때 객체 참조에 의존하면 구현이 어려워질 수 있지만, ID만 사용한다면 저장소나 기술이 달라져도 비교적 유연하게 대응할 수 있다.
그래서 DDD에서는 애그리거트 간에는 ID를 이용해 참조하는 방식을 권장한다.
애그리거트는 단순히 객체를 묶는 기술이 아니라는 것이다.
오히려 도메인의 경계를 명확하게 만들고, 변경의 범위를 통제하기 위한 설계 방법에 더 가깝다.
애그리거트 루트를 통해 상태를 변경하고, 트랜잭션과 리포지터리 역시 애그리거트 단위로 관리한다.
아직은 작은 프로젝트에서는 필요성을 크게 체감하지 못할 수도 있지만, 도메인이 복잡해질수록 왜 DDD에서 애그리거트를 중요한 개념으로 다루는지 조금씩 이해하게 되는 것 같다.