
도메인 주도 설계(DDD, Domain-Driven Design)에서 Aggregate는 일관성 경계(consistency boundary)를 정의하는 핵심 패턴입니다. Aggregate 경계를 잘못 설정하면 트랜잭션 충돌, 성능 저하, 높은 결합도라는 세 가지 문제가 동시에 발생합니다. 이 글에서는 Aggregate 경계를 실제 프로젝트에 적용할 때 발생하는 설계 판단 기준과 Java 코드 예제를 통해 경계 설정 전략을 구체적으로 살펴봅니다.
Aggregate란 무엇이고 왜 경계가 중요한가
Aggregate는 하나의 루트 엔티티(Aggregate Root)와 그에 속한 엔티티·값 객체(Value Object)의 묶음으로, 트랜잭션 일관성을 보장하는 단위입니다. 외부 객체는 반드시 Aggregate Root를 통해서만 내부 상태에 접근할 수 있습니다.
경계 설계가 중요한 이유는 단순합니다. Aggregate 하나는 하나의 트랜잭션 안에서만 변경이 보장됩니다. 여러 Aggregate를 하나의 트랜잭션으로 묶으면 잠금 경합이 늘어나고 확장성이 떨어집니다. 반대로 경계를 너무 잘게 나누면 결과적 일관성(eventual consistency)을 보장하는 로직이 애플리케이션 레이어로 흘러넘쳐 복잡도가 증가합니다.
핵심 원칙을 정리하면 다음과 같습니다.
- 한 트랜잭션 = 한 Aggregate 변경을 기본 원칙으로 삼습니다.
- 다른 Aggregate는 ID로만 참조하고, 객체 참조를 직접 보유하지 않습니다.
- Aggregate 크기는 비즈니스 불변식(invariant)의 범위에 맞춥니다.
경계 설계의 판단 기준: 불변식과 트랜잭션 범위
Aggregate 경계를 결정하는 가장 확실한 기준은 불변식(invariant) 입니다. "이 데이터들이 항상 함께 일관된 상태여야 하는가?"라는 질문에 "예"라고 답할 수 있는 범위가 곧 하나의 Aggregate입니다.
예를 들어 주문(Order) 도메인을 생각해 봅니다. Order와 OrderLine은 "주문 총액은 각 항목 금액의 합과 같아야 한다"는 불변식을 공유하므로 동일 Aggregate에 속합니다. 반면 Customer는 별도의 생명주기를 가지므로 독립 Aggregate로 분리하고, Order는 customerId만 보유합니다.
아래는 잘못된 설계와 올바른 설계를 비교한 예시입니다.
변경전 — Customer를 직접 참조 (결합도 높음, 트랜잭션 범위 과대)
// ❌ 다른 Aggregate를 객체 참조로 직접 보유
public class Order {
private Customer customer; // 직접 참조 → 트랜잭션 충돌 위험
private List<OrderLine> lines;
public void addLine(Product product, int quantity) {
// customer 상태가 바뀔 때마다 Order 트랜잭션에 영향
if (!customer.isActive()) throw new IllegalStateException("비활성 고객");
lines.add(new OrderLine(product, quantity));
}
}변경후 — ID 참조로 Aggregate 분리 (결합도 낮음, 경계 명확)
// ✅ 다른 Aggregate는 ID로만 참조
public class Order {
private final OrderId id;
private final CustomerId customerId; // ID 참조만 보유
private final List<OrderLine> lines = new ArrayList<>();
private Money totalAmount;
// 불변식: totalAmount == lines의 합
public void addLine(ProductId productId, int quantity, Money unitPrice) {
lines.add(new OrderLine(productId, quantity, unitPrice));
recalculateTotal(); // 내부 불변식 즉시 보장
}
private void recalculateTotal() {
this.totalAmount = lines.stream()
.map(OrderLine::subtotal)
.reduce(Money.ZERO, Money::add);
}
public Money getTotalAmount() { return totalAmount; }
}// 실행 결과 예시
Order order = new Order(OrderId.generate(), new CustomerId("C-001"));
order.addLine(new ProductId("P-001"), 2, Money.of(15000));
order.addLine(new ProductId("P-002"), 1, Money.of(30000));
System.out.println(order.getTotalAmount()); // Money{amount=60000, currency=KRW}Customer 활성 여부 검증은 애플리케이션 서비스 레이어에서 CustomerRepository를 통해 별도로 수행합니다. 이렇게 하면 두 Aggregate가 서로 다른 트랜잭션으로 처리되어 잠금 경합이 사라집니다.
경계를 넘는 일관성: 도메인 이벤트 활용
두 Aggregate 사이에서 상태 변경을 동기화해야 할 때는 도메인 이벤트(Domain Event) 를 사용합니다. 이는 결과적 일관성 패턴으로, 한 Aggregate의 변경이 완료된 뒤 이벤트를 발행하고 다른 Aggregate가 이를 구독하여 자신의 상태를 업데이트합니다.
// Order Aggregate Root에서 이벤트 발행
public class Order {
private final List<DomainEvent> domainEvents = new ArrayList<>();
public void place() {
// ... 주문 확정 로직
domainEvents.add(new OrderPlacedEvent(this.id, this.customerId, this.totalAmount));
}
public List<DomainEvent> pullDomainEvents() {
List<DomainEvent> events = List.copyOf(domainEvents);
domainEvents.clear();
return events;
}
}
// 별도 Aggregate인 CustomerPoint가 이벤트를 구독
@Component
public class CustomerPointEventHandler {
private final CustomerPointRepository pointRepository;
@EventListener
@Transactional // 별도 트랜잭션으로 처리
public void on(OrderPlacedEvent event) {
CustomerPoint point = pointRepository.findByCustomerId(event.customerId());
point.accrue(event.totalAmount()); // CustomerPoint Aggregate만 변경
pointRepository.save(point);
}
}이 패턴에서 Order와 CustomerPoint는 각자의 트랜잭션 안에서 독립적으로 일관성을 유지합니다. 이벤트 발행이 실패했을 때를 대비해 Outbox 패턴을 함께 적용하면 이벤트 유실 없이 결과적 일관성을 보장할 수 있습니다.
맺음말
Aggregate 경계 설계는 "얼마나 많은 것을 묶을 것인가"가 아니라 "어떤 불변식을 함께 보호할 것인가"에서 출발해야 합니다. 실제 프로젝트에서 자주 나타나는 실수는 ERD를 그대로 Aggregate로 옮기거나, 편의상 여러 Aggregate를 하나의 트랜잭션으로 처리하는 것입니다.
경계 설계 원칙을 요약하면 다음과 같습니다.
- 불변식이 같은 객체만 같은 Aggregate에 넣습니다.
- 다른 Aggregate는 ID로만 참조합니다.
- Aggregate 간 일관성은 도메인 이벤트와 결과적 일관성으로 처리합니다.
- 성능 문제가 발생하면 Aggregate 크기를 먼저 의심합니다.
Aggregate 설계를 더 깊이 이해하려면 Vaughn Vernon의 Implementing Domain-Driven Design과 공식 DDD 커뮤니티 자료인 dddcommunity.org를 참고하시기 바랍니다. Aggregate 경계가 명확해지면 마이크로서비스 분리 경계도 자연스럽게 따라온다는 점에서, 아키텍처 확장성 측면에서도 중요한 설계 기반이 됩니다.
'프로그래밍 PROGRAMMING > 아키텍쳐' 카테고리의 다른 글
| [SPRING/JAVA] Spring Boot 멀티모듈 프로젝트 구조 설계하기 (0) | 2026.04.02 |
|---|---|
| Event-Driven Architecture 적용하기 (0) | 2026.03.31 |
| Outbox 패턴으로 분산 시스템 이벤트 유실 없이 발행하기 (0) | 2026.03.25 |
| Event Sourcing 아키텍처 패턴 적용하기 (0) | 2026.03.23 |
| BFF 패턴으로 API 게이트웨이 설계하기 (0) | 2026.03.19 |