애그리거트의 정의와 본질 🔍
애그리거트는 연관된 객체들의 클러스터로, 도메인 모델에서 하나의 단위로 취급되는 패턴입니다. 에릭 에반스(Eric Evans)가 제안한 이 개념은 복잡한 도메인을 관리 가능한 단위로 분해하는 핵심 전략입니다.
애그리거트는 데이터 변경의 단위이자 트랜잭션의 일관성 경계로 작용합니다. 이는 단순한 객체 그룹이 아닌, 도메인 규칙과 불변식을 보장하는 응집력 있는 단위입니다.
애그리거트 루트(Aggregate Root)의 역할과 책임 ⚙️
1. 접근 제어자 (Gatekeeper)
- 모든 외부 참조는 루트를 통해서만 이루어짐
- 내부 엔티티에 대한 직접 접근 차단
- 예시: Order 루트를 통해서만 OrderLine 수정 가능
2. 일관성 관리자 (Consistency Manager)
- 도메인 규칙 검증 및 적용
- 애그리거트 내부 상태 변경 제어
- 예시: 주문 금액과 주문 항목 합계가 일치하는지 검증
3. 식별자 보유자 (Identity Holder)
- 전역적으로 식별 가능한 ID 보유
- 레포지토리에서 조회 시 기준점
- 예시: OrderId를 통한 주문 애그리거트 전체 로드
애그리거트 경계 설정의 기준 🔄
1. 비즈니스 불변식 기준
- 함께 변경되어야 하는 객체들을 그룹화
- 하나의 트랜잭션으로 일관성을 유지해야 하는 범위
- 예시: 주문 총액은 항상 주문 항목의 합계와 일치해야 함
2. 성능과 확장성 고려
- 너무 큰 애그리거트: 성능 저하, 동시성 문제
- 너무 작은 애그리거트: 일관성 관리 어려움
- 적절한 크기는 도메인 특성에 따라 결정
3. 생명주기 일치성
- 함께 생성되고 함께 삭제되는 객체들
- 동일한 생명주기를 가진 객체들을 그룹화
- 예시: 주문과 주문 항목은 함께 생성되고 삭제됨
애그리거트 간 참조 전략 🔗
1. ID 참조 (권장)
public class Order {
private CustomerId customerId; // ID 참조
// ...
}
2. 직접 참조 (제한적 사용)
public class Order {
private Customer customer; // 직접 참조
// ...
}
3. 참조 전략 선택 기준
- 일관성 요구사항: 강한 일관성 필요 시 같은 애그리거트에 포함
- 성능 요구사항: 자주 함께 로드되는 객체는 직접 참조 고려
- 확장성: 시스템 규모가 클수록 ID 참조 선호
애그리거트 패턴의 구체적 구현 예시 💻
주문 애그리거트 상세 예시
// 애그리거트 루트
public class Order {
private OrderId id;
private CustomerId customerId;
private List<OrderLine> orderLines;
private ShippingInfo shippingInfo;
private OrderStatus status;
private Money totalAmount;
// 애그리거트 내부 일관성 유지 메서드
public void addOrderLine(Product product, int quantity) {
// 비즈니스 규칙 검증
if (status != OrderStatus.CREATED) {
throw new IllegalStateException("이미 주문이 확정되었습니다.");
}
// 내부 엔티티 변경
OrderLine line = new OrderLine(product.getId(), product.getPrice(), quantity);
orderLines.add(line);
// 불변식 유지
recalculateTotalAmount();
}
private void recalculateTotalAmount() {
this.totalAmount = orderLines.stream()
.map(OrderLine::getAmount)
.reduce(Money.ZERO, Money::add);
}
// 외부에서 접근 가능한 행동 메서드
public void changeShippingAddress(Address newAddress) {
if (status != OrderStatus.CREATED && status != OrderStatus.PREPARING) {
throw new IllegalStateException("배송 중에는 주소를 변경할 수 없습니다.");
}
this.shippingInfo.changeAddress(newAddress);
}
}
// 내부 엔티티
public class OrderLine {
private ProductId productId;
private Money price;
private int quantity;
private Money amount;
public OrderLine(ProductId productId, Money price, int quantity) {
this.productId = productId;
this.price = price;
this.quantity = quantity;
this.amount = price.multiply(quantity);
}
// 제한된 기능만 제공
Money getAmount() {
return amount;
}
}
애그리거트 저장 및 조회 전략 💾
1. 저장소 분리 전략
- 애그리거트당 하나의 저장소(Repository) 구현
- 애그리거트 루트만 저장소를 통해 접근 가능
- 예시: OrderRepository, ProductRepository
2. 데이터베이스 매핑 전략
- 단일 테이블 전략: 작은 애그리거트에 적합
- 테이블 분리 전략: 복잡한 애그리거트에 적합
- NoSQL 활용: 문서 지향 DB가 애그리거트 개념과 잘 맞음
3. 지연 로딩과 즉시 로딩
- 애그리거트 내부는 즉시 로딩 고려
- 애그리거트 간 참조는 지연 로딩 고려
- 성능과 일관성 사이의 균형 필요
애그리거트의 트랜잭션 관리 🔄
1. 단일 애그리거트 수정 원칙
- 하나의 트랜잭션에서는 하나의 애그리거트만 수정
- 여러 애그리거트 수정 시 일관성 문제 발생 가능
2. 다중 애그리거트 수정이 필요한 경우
- 도메인 이벤트 활용: 비동기 일관성 유지
- Saga 패턴 적용: 장기 실행 트랜잭션 관리
- 결과적 일관성 허용: 즉시 일관성이 필요하지 않은 경우
3. 낙관적 동시성 제어
@Entity
public class Order {
@Id
private String id;
@Version
private Long version;
// ...
}
애그리거트 설계 시 흔한 실수와 해결책 ⚠️
1. 과도하게 큰 애그리거트
- 문제: 성능 저하, 동시성 충돌 증가
- 해결: 비즈니스 요구사항에 따라 여러 애그리거트로 분리
2. 애그리거트 간 직접 참조 남용
- 문제: 경계 모호, 의존성 복잡화
- 해결: ID 참조 사용, 필요시 조회 서비스 구현
3. 기술 중심 설계
- 문제: ORM 매핑 편의성에 따른 설계 왜곡
- 해결: 도메인 모델 우선 설계, 기술은 구현 수단으로만 활용
실전 애그리거트 설계 프로세스 🛠️
- 도메인 이벤트 스토밍으로 핵심 개념 식별
- 비즈니스 불변식 분석으로 경계 설정
- 애그리거트 루트 식별 및 책임 할당
- 내부 엔티티와 값 객체 설계
- 애그리거트 간 관계 정의 (ID 참조 vs 직접 참조)
- 저장소 인터페이스 설계
- 트랜잭션 경계 설정
마이크로서비스와 애그리거트 🌐
애그리거트는 마이크로서비스 경계 설정의 중요한 기준이 됩니다. 하나의 마이크로서비스는 하나 이상의 응집력 있는 애그리거트를 포함하며, 서비스 간 통신은 ID 참조와 이벤트 기반 통신으로 구현됩니다.
'Back-End' 카테고리의 다른 글
| [Redis] 기본 개념 (1) | 2025.05.26 |
|---|---|
| [Java] JPA 영속성 (1) | 2025.05.23 |
| [Spring] Mybatis (1) | 2025.05.08 |
| [Spring] 어노테이션 세부사항 정리 (1) | 2025.04.24 |
| [Spring] AOP (1) | 2025.04.23 |