『도메인 주도 개발 시작하기 - DDD 핵심 개념 정리부터 구현까지』를 읽고 정리한 내용입니다.
애그리거트(Aggregate)
애그리거트는 관련된 객체를 하나의 단위로 묶은 것을 의미한다.
애그리거트는 단순히 관련된 객체를 묶는 개념이 아니라, 도메인 규칙을 기반으로 일관성을 유지하기 위한 경계(Boundary) 를 의미한다.
따라서 애그리거트의 경계는 객체 간의 관계만으로 결정하는 것이 아니라, 도메인의 규칙과 요구사항을 기준으로 결정해야 한다.
NOTE
애그리거트는 일관성을 유지하기 위한 경계(Boundary)이다.
애그리거트의 경계는 도메인 규칙과 요구사항에 따라 결정한다.
특징
애그리거트는 다음과 같은 특징을 가진다.
- 관련된 객체를 하나의 단위로 관리한다.
- 모델을 개별 객체뿐만 아니라 상위 수준에서도 이해할 수 있도록 한다.
- 애그리거트 내부의 객체들은 동일하거나 유사한 라이프 사이클을 가진다.
- 도메인 규칙과 요구사항에 따라 경계를 설정한다.
예를 들어 주문과 관련된 객체는 하나의 애그리거트로 구성할 수 있다.
Order (Aggregate)
├── Order
├── OrderItem
├── ShippingInfo
└── PaymentInfo
애그리거트 루트(Aggregate Root)
애그리거트 루트는 애그리거트를 대표하는 객체를 의미한다.
애그리거트 외부에서는 내부 객체를 직접 변경하지 않고, 애그리거트 루트를 통해서만 내부 상태를 변경한다.
NOTE
애그리거트 외부에서는 내부 객체를 직접 변경하지 않는다.
모든 상태 변경은 Aggregate Root를 통해 수행하여 애그리거트의 일관성을 유지한다.
역할
애그리거트 루트는 다음과 같은 역할을 담당한다.
- 애그리거트의 일관성을 유지한다.
- 애그리거트가 제공하는 도메인 기능을 제공한다.
- 애그리거트 내부 객체의 상태를 관리한다.
외부
│
▼
Order (Root)
│
├── OrderItem
├── Delivery
└── Payment
✔ Root를 통해서만 접근
✖ 내부 객체 직접 변경
특징
애그리거트의 일관성을 유지하기 위해 다음과 같은 방식을 적용할 수 있다.
public setter사용을 지양한다.- Value Object는 가능한 불변 객체로 설계한다.
- 내부 객체의 상태 변경은 애그리거트 루트를 통해 수행한다.
예를 들어 외부에서 OrderItem의 상태를 직접 변경하는 것이 아니라 Order가 제공하는 도메인 기능을 통해 변경하도록 구현한다.
order.changeItemQuantity(itemId, quantity);
이렇게 하면 상태 변경 과정에서 필요한 도메인 규칙을 애그리거트 루트가 검증할 수 있다.
트랜잭션과 애그리거트
하나의 트랜잭션에서는 하나의 애그리거트만 수정하는 것을 권장한다.
O Application Service
├── Order 수정
└── Member 수정
X Order → Member 직접 수정
여러 애그리거트를 하나의 트랜잭션에서 수정해야 하는 경우에는 애그리거트가 서로 직접 변경하도록 구현하지 않는다.
대신 응용 계층(Application Service)에서 각 애그리거트를 조회하고 필요한 작업을 조율하도록 구현한다.
NOTE
여러 애그리거트를 함께 수정해야 한다면 애그리거트가 직접 다른 애그리거트를 변경하는 것이 아니라 응용 계층(Application Service) 에서 조율하도록 구현한다.
즉, 두 개 이상의 애그리거트 변경이 필요한 경우 응용 계층이 각 애그리거트의 작업을 조합하여 하나의 유스케이스를 수행한다.
리포지토리와 애그리거트
리포지토리는 애그리거트 단위로 존재하는 것이 기본이다.
리포지토리는 애그리거트의 생명주기를 관리하며, 애그리거트를 저장하고 조회하는 역할을 담당한다.
역할
리포지토리는 다음과 같은 기능을 제공한다.
- 애그리거트를 조회한다.
- 애그리거트를 저장한다.
- 애그리거트의 변경 사항을 영속성에 반영한다.
Application
│
▼
OrderRepository
│
▼
Order Aggregate
응용 계층은 리포지토리를 통해 애그리거트를 조회하거나 저장한다.
이때 리포지토리의 구체적인 구현은 인프라 계층에서 담당할 수 있으며, 응용 계층은 리포지토리의 인터페이스에 의존하도록 구성할 수 있다.
ID를 이용한 애그리거트 참조
애그리거트 간의 관계를 표현할 때 다른 애그리거트를 객체로 직접 참조하는 방법과 ID를 이용해 참조하는 방법이 있다.
객체 참조
다른 애그리거트를 객체로 직접 참조하는 방식이다.
class Order {
private Member member;
}
이 방식은 객체 간 관계를 자연스럽게 표현할 수 있다는 장점이 있다.
하지만 애그리거트 간의 직접적인 의존 관계가 만들어진다는 문제가 있다.
문제점
객체 참조를 사용하면 다음과 같은 문제가 발생할 수 있다.
- 애그리거트 간 결합도가 증가한다.
- 즉시 로딩과 지연 로딩을 고려해야 한다.
- 객체 그래프가 커지면서 관리가 어려워질 수 있다.
- 저장소나 구현 기술을 변경할 때 영향을 받을 수 있다.
이러한 문제를 줄이기 위해 다른 애그리거트를 직접 참조하는 대신 ID를 이용해 참조할 수 있다.
ID 참조
class Order {
private MemberId memberId;
}
이 방식에서는 Order가 Member 객체를 직접 참조하지 않고 MemberId만 가지고 있다.
장점
ID 참조를 사용하면 다음과 같은 장점이 있다.
- 애그리거트 간 직접적인 의존을 줄일 수 있다.
- 각 애그리거트의 응집도를 높일 수 있다.
- 애그리거트별로 독립적인 저장소를 사용하는 것이 쉬워진다.
- 불필요한 객체 로딩을 방지할 수 있다.
단점
ID를 이용해 다른 애그리거트를 참조하면 필요한 객체를 별도로 조회해야 한다는 단점이 있다.
Order 조회
│
▼
memberId 획득
│
▼
Member 조회
따라서 여러 애그리거트의 데이터를 함께 조회해야 하는 경우 추가 조회가 발생할 수 있다.
해결 방법
이러한 문제는 조회 목적에 맞는 별도의 방식을 사용하여 해결할 수 있다.
- 조회 전용 쿼리 사용
- JPQL 사용
- MyBatis 사용
- 캐시 사용
- 조회 전용 저장소 구성
즉, 애그리거트의 변경을 위한 모델과 조회를 위한 모델을 분리하여 각각의 목적에 맞게 구현할 수 있다.
애그리거트 간 집합 연관
애그리거트 간에 1:N이나 M:N과 같은 집합 연관이 존재하더라도 이를 반드시 객체 연관으로 구현할 필요는 없다.
1:N 연관
예를 들어 하나의 Category에 여러 Product가 속하는 관계를 생각할 수 있다.
Category
│
└── Product 목록
개념적으로는 Category와 Product 사이에 1:N 관계가 존재한다.
하지만 이를 반드시 Category가 Product 목록을 직접 가지고 있는 객체 구조로 구현할 필요는 없다.
class Category {
private List<Product> products;
}
오히려 상품 목록을 조회하는 경우에는 별도의 조회 쿼리를 사용하는 것이 더 효율적인 경우가 있다.
즉, 개념적인 연관과 객체 간 직접적인 참조는 반드시 동일할 필요가 없다.
M:N 연관
M:N 관계는 하나의 객체가 여러 객체와 관계를 맺고, 반대쪽 객체 역시 여러 객체와 관계를 맺는 경우를 의미한다.
RDBMS에서는 일반적으로 조인 테이블을 사용하여 M:N 관계를 표현한다.
예를 들어 하나의 상품이 여러 카테고리에 속하고, 하나의 카테고리가 여러 상품을 포함할 수 있다면 다음과 같은 구조로 표현할 수 있다.
Product
│
Product_Category
│
Category
Product_Category와 같은 조인 테이블을 통해 두 애그리거트 사이의 M:N 관계를 표현할 수 있다.
이때도 객체 간 직접적인 참조를 무조건 사용하는 것보다 애그리거트의 경계와 조회 요구사항을 기준으로 관계를 설계하는 것이 중요하다.
정리
애그리거트는 관련된 객체를 하나의 단위로 묶고 일관성을 유지하기 위한 경계를 의미한다.
애그리거트의 핵심은 경계를 어떻게 설정하고, 내부의 일관성을 어떻게 유지할 것인가에 있다. 애그리거트 루트는 외부에서 애그리거트 내부 객체를 직접 변경하지 못하도록 하고, 필요한 상태 변경과 도메인 기능을 제공한다.
또한 애그리거트 간의 직접적인 객체 참조보다는 ID를 이용한 참조를 통해 애그리거트 간 결합도를 낮출 수 있다. 여러 애그리거트를 함께 수정해야 하는 경우에는 애그리거트가 서로를 직접 변경하기보다 응용 계층에서 각 애그리거트의 작업을 조율하는 것이 중요하다.
결국 애그리거트는 단순히 여러 객체를 묶는 개념이 아니라, 도메인 규칙에 따라 경계를 설정하고 일관성을 유지하기 위한 설계 단위라고 이해할 수 있다.