[독후감] 도메인 주도 개발 시작하기 챕터 03
애그리거트
애그리거트는 모델을 이해하는 데 도움을 줄 뿐만 아니라 일관성을 관리하는 기준도 된다. 모델을 보다 잘 이해할 수 있고 애그리거트 단위로 일관성을 관리하기 때문에, 애그리거트는 복잡한 도메인을 단순한 구조로 만들어준다.
애그리거트는 관련된 모델을 하나로 모았기 때문에 한 애그리거트에 속한 객체는 유사하거나 동일한 라이프 사이클을 갖는다. 애그리거트에 속한 구성요소는 대부분 함께 생성하고 함께 제거한다.
애그리거트는 경계를 갖는다. 한 애그리거트에 속한 객체는 다른 애그리거트에 속하지 않는다. 애그리거트는 독립된 객체 군이며 각 애그리거트는 자기 자신을 관리할 뿐 다른 애그리거트를 관리하지 않는다. 예를 들어 주문 애그리거트는 배송지를 변경하거나 주문 상품 개수를 변경하는 등 자기 자신은 관리하지만, 주문 애그리거트에서 회원의 비밀번호를 변경하거나 상품의 가격을 변경하지는 않는다.
경계를 설정할 때 기본이 되는 것은 도메인 규칙과 요구사항이다. 도메인 규칙에 따라 함께 생성되는 구성요소는 한 애그리거트에 속할 가능성이 높다.
이때, 흔히 'A가 B를 갖는다'로 설계할 수 있는 요구사항이 있다면 A와 B를 한 애그리거트로 묶어서 생각하기 쉽다. 하지만 반드시 A와 B가 한 애그리거트에 속한다는 것을 의미하는 것은 아니다.
좋은 예시가 상품과 리뷰이다. 상품 페이지에 가보면 리뷰 내용을 보여줘야 한다는 요구사항이 있을 때 상품 엔티티와 리뷰 엔티티가 한 애그리거트에 있다고 생각할 수 있다. 하지만 잘 생각해 보자. 상품이 생성될 때 리뷰가 함께 생성되는가? 또는 함께 변경되는가? 둘 다 아니다. 게다가 상품과 리뷰를 변경하는 주체는 각자 상품 담당자와 고객으로 생성, 변경 주체 또한 다르다.
애그리거트의 실제 크기는 도메인 규칙을 제대로 이해할 수록 줄어든다. 이 책의 저자는 다수의 애그리거트가 한 개의 엔티티 객체만 갖는 경우가 많으며 두 개 이상의 엔티티로 구성되는 애그리거트는 드물다고 하였다.
애그리거트 루트
애그리거트에 속한 모든 객체는 일관성을 유지해야 한다. 그러기 위해선 애그리거트 전체를 관리할 주체가 필요한데, 이 책임을 지는 것이 애그리거트 루트 엔티티이다. 애그리거트에 속한 객체는 애그리거트 루트 엔티티에 직간접적으로 속하게 된다.
애그리거트 루트의 핵심 역할은 애그리거트의 일관성이 깨지지않도록 하는 것이다. 이를 위해 애그리거트 루트는 애그리거트가 제공해야 할 도메인 기능을 구현한다. 예를 들면 주문 애그리거트는 배송지 변경, 상품 변경과 같은 기능을 제공하고, 애그리거트 루트인 Order가 이 기능을 구현한 메서드를 제공한다. 이때, 이 메서드는 도메인 규칙에 따라 애그리거트에 속한 객체의 일관성이 깨지지 않도록 구현해야 한다.
애그리거트 외부에서 애그리거트에 속한 객체를 직접 변경하면 안된다. 이것은 애그리거트 루트가 강제하는 규칙을 적용할 수 없어 모델의 일관성을 깨는 원인이 된다. 하지만 이를 해결하기 위해 응용 서비스에서 상태 확인 로직등을 구현할 수 있지만 이는 동일한 검사 로직을 여러 응용 서비스에서 중복으로 구현할 가능성이 높아져 유지보수에 도움이 되지 않는다.
불필요한 중복을 피하고 애그리거트 루트를 통해서만 도메인 로직을 구현하게 만들려면 도메인 모델에 대해 다음의 두 가지를 습관적으로 적용해야 한다.
- 단순히 필드를 변경하는 set 메서드를 공개(public) 범위로 만들지 않는다.
- 밸류 타입은 불변으로 구현한다.
트랜잭션 범위
한 트랜잭션에서 한 애그리거트만 수정해야 한다. 한 트랜잭션에서 두 개 이상의 애그리거트를 수정하면 트랜잭션 충돌이 발생할 가능성이 높아진다.
한 트랜잭션에서 한 애그리거트만 수정한다는 건 애그리거트에서 다른 애그리거트를 변경해하지 않는다는 것을 의미한다.
이는 애그리거트가 자신의 책임 범위를 넘어 다른 애그리거트의 상태까지 관리하는 꼴이 되기 때문이다.
부득이하게 한 트랜잭션에서 두 개 이상의 애그리거트를 수정해야 한다면 애그리거트에서 다른 애그리거트를 직접 수정하지 말고 응용 서비스에서 두 애그리거트를 수정하도록 구현해야 한다.
리포지터리와 애그리거트
애그리거트는 개념상 완전한 한 개의 도메인 모델을 표현하므로 객체의 영속성을 처리하는 리포지터리는 애그리거트 단위로 존재한다.
예를 들어 Order와 OrderLine을 물리적으로 각각 별도의 DB 테이블에 저장한다고 해서 Order와 OrderLine을 위한 리포지터리를 각각 만들지 않는다. Order가 애그리거트 루트 이므로 Order를 위한 리포지터리만 존재하면 된다.
ID를 이용한 애그리거트 참조
ORM 기술 덕에 애그리거트 루트에 대한 참조를 쉽게 구현할 수 있고 필드(또는 get메서드)를 이용한 애그리거트 참조를 사용하면 다른 애그리거트의 데이터를 쉽게 조회할 수 있다. 하지만 필드를 이용한 애그리거트 참조는 다음 문제를 야기할 수 있다.
- 편한 탐색 오용
- 성능에 대한 고민
- 확장 어려움
편한 탐색 오용의 문제점은 바로 이것이다. 한 애그리거트 내부에서 다른 애그리거트 객체에 접근할 수 있으면 다른 애그리거트의 상태를 쉽게 변경할 수 있게 된다. 애그리거트가 관리하는 범위는 자기 자신으로 한정해야 한다. 그런데 애그리거트 내부에서 다른 애그리거트 객체에 접근할 수 있으면 구현의 편리함 때문에 다른 애그리거트를 수정하고자 하는 유혹에 빠지기 쉽다.
성능에 대한 고민으로는 애그리거트를 직접 참조하게되면 성능과 관련된 여러 가지 고민을 해야 한다는 것이다.
확장의 어려움으로는 하나의 DB로 유지가 가능한 서비스가 커졌을 때가 해당한다. 사용자가 늘어나면서 여러 개의 DB가 필요해졌을 때 문제에 봉착하게 된다.
이러한 세 가지 문제를 완화하기 위해 사용할 것이 ID를 이용해서 다른 애그리거트를 참조하는 것이다. DB 테이블에서 외래키로 참조하는 것과 비슷하게 ID를 이용한 참조는 다른 애그리거트를 참조할 때 ID를 사용한다.
ID 참조를 사용하면 모든 객체가 참조로 연결되지 않고 한 애그리거트에 속한 객체들만 참조로 연결된다. 이는 애그리거트의 경계를 명확히 하고 애그리거트 간 물리적인 연결을 제거하기 때문에 모델의 복잡도를 낮춰준다. 또한 애그리거트 간의 의존을 제거하므로 응집도를 높여주는 효과도 있다.
구현 복잡도 또한 낮아진다. 다른 애그리거트를 직접 참조하지 않으므로 애그리거트 간 참조를 지연 로딩으로 할지 즉시 로딩으로 할지 고민하지 않아도 된다. 참조하는 애그리거트가 필요하면 응용 서비스에서 ID를 이용해서 로딩하면 된다.
이게 대체 무슨 소리냐....?
멘붕이 갑자기 오기 시작했다.
역시 이럴때는 챗GPT 만한 게 없다.
1. 참조를 이용한 경우
class Order {
List<OrderItem> orderItems;
// 다른 속성들...
}
class OrderItem {
// 주문 상품 정보...
}
이 경우엔 Order가 OrderItem를 직접 참조하고 있다. 이러한 구조는 주문 애그리거트 내의 모든 주문 상품을 관리하기 위해 주문 객체를 로드해야 한다는 불편함이 있다.
2. ID참조를 사용한 경우
class Order {
List<OrderItemId> orderItemIds;
// 다른 속성들...
}
여기서 OrderItemId가 ID참조를 사용한 것이다. 주문 애그리거트 내에서 주문 상품 ID만을 가지고 있기 때문에, 주문 상품의 세부 정보를 필요로 할 때는 필요한 주문 상품 ID를 사용하여 해당 주문 상품을 검색하거나 로드할 수 있다. 이렇게 하면 주문 객체를 로드할 때 주문 상품을 함께 로드하지 않아도 되므로 효율적인 DB 액세스 및 메모리 사용이 가능하다.
이 설명을 보면 이해가 어려운 사람들에게 조금이나마 이해에 도움이 될 거 같다.
또한 PK의 개념과 헷갈려서 gpt에게 물어본 결과이다.
ID를 이용한 참조 방식을 사용하면 복잡도를 낮추는 것과 함께 한 애그리거트에서 다른 애그리거트를 수정하는 문제를 근원적으로 방지할 수 있다. 외부 애그리거트를 직접 참조하지 않기 때문에 애초에 한 애그리거트에서 다른 애그리거트의 상태를 변경할 수 없기 때문이다.
애그리거트를 팩토리로 사용하기
애그리거트가 갖고 있는 데이터를 이용해서 다른 애그리거트를 생성해야 한다면 애그리거트에 팩토리 메서드를 구현하는 것을 고려해 보자. 책의 예시로 Product와 Store가 나오는데 이쪽은 꼭 책을 읽어보도록 하자.