[도메인 주도 설계] (7) 기본 구성 요소 4 – 애그리게이트

애그리게이트는 함께 변경되어야만 올바른 데이터의 일관성 경계를 명확히 정의해요. 절대 허용할 수 없는 불일치 상태를 원천 차단하는 설계 방법을 배워요.

'Domain-Driven DESIGN'이라는 컬러 타이포그래피 로고. 하늘색 배경에 '도메인 주도 설계 — (7) 기본 구성 요소 4 - 애그리게이트'라는 텍스트가 적혀 있다.

지난 편에서 모듈로 도메인을 조직하는 방법을 살펴봤어요. 이번 편에서는 애그리게이트(Aggregate)가 무엇인지, 루트·경계·내부 객체의 구조, 설계 규칙, 크기 결정법, 그리고 여러 애그리게이트가 필요한 이유까지 구체적인 예시와 함께 알아볼게요.


1. 애그리게이트란 무엇인가: 함께 참이어야 하는 것

대부분의 제품에서 하나의 사용자 액션은 한 가지만 바꾸는 경우가 드물어요.

채용 플랫폼에서 구체적인 예를 볼게요. 지원서가 면접(INTERVIEW) 단계로 이동할 때, 두 가지가 보통 함께 일어나요.

이 두 변경은 함께일 때만 의미가 있어요.

상태가 “면접”인데 면접 시간이 없으면, 제품이 고장난 것처럼 느껴져요. 면접 시간이 있는데 상태가 여전히 “서류 심사”라면, 그것도 마찬가지로 혼란스럽죠.

이것이 애그리게이트(Aggregate)가 해결하려는 문제예요.

“이것들은 항상 함께 검사되고 함께 변경되어야 한다.”

애그리게이트 안에서는 변경을 하나의 단위로 다뤄요. 관련된 모든 규칙이 통과하면 변경이 성공하고, 그렇지 않으면 시스템이 아무 변경도 적용하지 않아요.

애그리게이트를 비유하면, 비행기 이륙 전 체크리스트와 같아요. 연료 확인, 엔진 상태, 승무원 배치, 활주로 허가가 모두 통과해야 이륙할 수 있어요. “연료는 됐으니 일단 이륙하고, 승무원은 나중에 확인하자”는 허용되지 않죠. 어느 하나라도 실패하면 이륙 전체가 중단돼요.


2. 애그리게이트 루트, 경계, 내부 객체: 누가 무엇을 바꿀 수 있는가

구성 요소 정의 존재 이유
애그리게이트 루트 외부에 노출되는 메인 엔티티 모든 규칙이 한 곳에서 강제되도록 보장
경계 애그리게이트를 둘러싼 개념적 경계 함께 일관되어야 하는 것을 정의
내부 객체 경계 안의 엔티티 또는 값 객체 외부 코드가 규칙을 우회하는 것을 방지

1) 애그리게이트 루트

애그리게이트 루트(Aggregate Root)는 외부에 노출되는 메인 엔티티예요. 모든 규칙이 한 곳에서 강제되도록 보장하죠.

시스템의 다른 부분이 변경을 원하면, 내부 조각을 직접 업데이트할 수 없어요. 루트에게 요청해야 하죠. 이것이 제품에 변경에 대해 “예” 또는 “아니오”를 말할 수 있는 단일 지점을 제공해요.

2) 경계

경계(Boundary)는 동시에 참이어야 하는 범위를 정의해요.

경계 안의 모든 것은 함께 변경되는 것으로 가정돼요. 한 부분이 무효화되면, 전체 변경이 거부되죠. 이것이 개별적으로는 괜찮아 보이지만 조합하면 의미 없는 상태를 방지해요.

3) 내부 객체

내부 객체(Internal Objects)는 애그리게이트의 행동을 지원하기 위해 존재하지, 독립적으로 관리되기 위해 존재하지 않아요.

자체 구조와 규칙을 가질 수 있지만, 단독으로 업데이트되지 않아요. 외부 코드가 직접 수정할 수 있다면, 중요한 검증을 건너뛰고 제품을 깨진 상태로 두기 쉬워지죠.

이렇게 보면 애그리게이트는 기술적 컨테이너가 아니에요. 결정 경계예요.


3. 애그리게이트 설계 규칙

애그리게이트를 설계할 때 따라야 할 핵심 규칙들이에요.

규칙 의미 왜 중요한가
불변 조건 보호 항상 유지되어야 하는 모든 규칙이 애그리게이트 안에서 강제됨 무효한 상태가 시스템에 들어오는 것을 방지
외부 접근 제한 외부에서는 애그리게이트 루트만 접근 가능 규칙이 우회되는 것을 차단
정체성 범위 관리 루트는 전역 정체성을 갖지만, 내부 객체는 갖지 않음 경계를 명확히 하고 결합도를 줄임
단위로 삭제 루트를 삭제하면 전체 애그리게이트가 제거됨 떠돌거나 의미 없는 데이터를 방지
트랜잭션 경계 하나의 트랜잭션에서 하나의 애그리게이트만 수정 일관성 비용을 명시적으로 만듦
루트만 참조 애그리게이트는 다른 애그리게이트의 루트만 참조 숨겨진 경계 간 의존성을 방지

4. 예시: 여행 플래너의 여행 일정 애그리게이트

개인 여행 일정 플래너를 상상해 보세요. 여러 날에 걸친 여행을 계획하는 제품이에요.

이 제품에서는 이런 특성이 있어요.

다시 말해, 이 제품은 여행자, 구간, 일정을 독립적인 객체로 다루지 않아요. 모든 것이 “이 여행”이라는 범위에 속하죠.

여행 일정의 중심에는 Itinerary가 있어요. 여행 일정은 단순한 날짜 목록이 아니에요. 전체적으로 항상 의미가 통해야 하는 계획이에요. 그래서 애그리게이트로 잘 작동하죠.

Itinerary 애그리게이트
─────────────────────

┌──────────────────────────────────────────┐
│   Itinerary (애그리게이트 루트, 엔티티)        │
│------------------------------------------│
│ 식별자: itineraryId                        │
│                                          │
│ 불변 조건:                                 │
│ - 구간이 여행 날짜 안에 맞아야 함               │
│ - 총 기간이 항상 계산 가능해야 함               │
│ - 여행자 변경이 계획을 유효하게 유지해야 함        │
└───────────────┬──────────────────────────┘
                │ 소유
        ┌───────┼───────────────┐
        ▼                       ▼
┌──────────────────┐   ┌──────────────────┐
│ TripSegment      │   │ TravelerDetails  │
│ (값 객체)          │   │ (값 객체)         │
│------------------│   │------------------│
│ dateRange        │   │ 여행자 정보        │
│ location         │   │ 선호 사항          │
└──────────┬───────┘   └──────────────────┘
           │ 사용
           ▼
┌──────────────────┐
│ DateRange        │
│ (값 객체)          │
│------------------│
│ startDate        │
│ endDate          │
└──────────────────┘

Itinerary 자체가 애그리게이트 루트예요. 제품 관점에서 사용자가 “내 여행”으로 인식하는 것이죠.

모든 의미 있는 변경이 여행 일정을 통해 이루어져요. 여행 일정이 속한 맥락을 고려하지 않고 구간이나 여행자를 독립적으로 편집하는 개념은 없죠.


5. 애그리게이트 크기 결정: 가장 작게 함께 일관되어야 하는 집합

일반적인 경험 법칙은 애그리게이트를 작게 유지하는 것이에요. 큰 애그리게이트는 실질적인 트레이드오프가 따르기 때문이에요.

크기를 결정할 때 유용한 질문들은 다음과 같아요.

질문 밝히는 것
이것들이 항상 함께 바뀌는가? 아니라면, 함께 있을 필요가 없을 가능성이 높다
잠깐이라도 동기화가 어긋나면 무엇이 깨지는가? 즉각적인 고장이면 하나의 애그리게이트를 시사
사용자가 즉시 알아차리는가? 사용자에게 보이는 비일관성이 중요하다
조율이 비용이 커지고 있는가? 애그리게이트가 너무 클 수 있다

“올바른” 크기는 이래요.

도메인 규칙을 유효하게 유지하기 위해 함께 변경되어야 하는 가장 작은 객체 집합

그보다 크면 결합도, 자원 경쟁(Contention), 비용이 증가해요.


6. 애그리게이트 vs 모듈: 일관성 vs 조직화

처음에는 애그리게이트가 모듈처럼 보여요. 둘 다 관련 개념을 그룹화하고, 경계를 그리고, 복잡성을 줄이니까요. 하지만 존재하는 이유가 매우 달라요.

측면 모듈 애그리게이트
주요 목적 코드와 책임을 조직 일관성 규칙을 보호
경계의 의미 소유권과 구조 함께 올바라야 하는 것
트랜잭션 범위 여러 모듈에 걸칠 수 있음 하나의 애그리게이트로 제한
실패 영향 보통 기술적 사용자에게 보이는 제품 비일관성
변경 동기 유지보수성 사용 중 정상 작동 보장

모듈은 이 질문에 답해요.

“이 로직이 어디에 살아야 하는가?”

애그리게이트는 이 질문에 답해요.

“어떤 변경이 함께 성공하거나 함께 실패해야 하는가?”

제품 관점에서 이 구분이 중요한 이유는, 애그리게이트가 일관성을 명시적인 제품 결정으로 바꾸기 때문이에요. 즉각적인 정확성이 필요한 곳과 그렇지 않은 곳을 팀이 선택하도록 강제하죠.


7. 여러 애그리게이트가 나타나는 이유: 다른 종류의 제품 약속

여러 애그리게이트는 제품이 다른 종류의 약속을 동시에 하기 시작할 때 나타나요.

이 모든 것을 하나의 애그리게이트 안에서 강제하려 하면 충돌이나 긴장 관계 생겨요.

다음과 같은 질문이 들리기 시작하면, 이미 여러 애그리게이트를 다루고 있는 거예요.

각 애그리게이트는 하나의 핵심 불변 조건 집합을 보호하기 위해 존재하고, 어떤 단일 경계도 현실적으로 모든 것을 취약해지지 않고 담을 수 없어요.


8. 예시: 여행 일정 vs 예약 vs 결제 (이벤트와 비동기 현실)

같은 여행 플래너가 진화한다고 상상해 보세요. 처음에는 여행 계획만 했는데, 이후 실제 예약결제 기능이 추가돼요. 이 시점에서 도메인이 근본적으로 바뀌죠.

예약과 결제가 생기면 이런 현실이 나타나요.

계획과 실행이 더 이상 같은 것이 아니에요. 여기서 여러 애그리게이트가 필수적이 되죠.

세 개의 애그리게이트가 나타나요.

애그리게이트 나타내는 것 항상 참이어야 하는 것
여행 일정(Itinerary) 사용자의 여행 계획 계획이 전체적으로 의미가 통한다
예약(Booking) 실제 세계의 예약 상태가 외부 약속을 반영한다
결제(Payment) 돈의 이동 이중 청구 없음, 환불 누락 없음

전형적인 흐름은 이래요.

  1. 사용자가 여행 일정을 편집Itinerary 애그리게이트 업데이트 (계획 규칙이 검증되고 원자적으로 저장)
  2. 사용자가 구간을 확정Booking 애그리게이트 생성 (초기 상태: PENDING_PAYMENT)
  3. 사용자가 결제를 진행Payment 애그리게이트가 비동기로 청구 시도
  4. 결제 성공결제 성공 이벤트 발행
  5. Booking이 결제 결과에 반응Booking 애그리게이트가 자체 상태를 CONFIRMED로 업데이트
  6. 사용자가 여행 일정을 다시 조회Itinerary가 업데이트된 예약 상태를 반영

어떤 시점에서도 하나의 애그리게이트가 다른 애그리게이트의 내부를 직접 수정하지 않아요. 각 애그리게이트가 자체 트랜잭션에서 변경되고, 공유 상태가 아닌 참조와 이벤트를 통해 조율되죠.

각 애그리게이트는 다른 불변 조건을 보호하고, 실패의 비용도 달라요.

여러 애그리게이트
─────────────────

┌──────────────────────────────────────────┐
│     Itinerary (애그리게이트 루트)             │
│------------------------------------------│
│ 식별자: itineraryId                        │
│                                          │
│ 불변 조건:                                 │
│ - 구간이 여행 날짜에 맞음                     │
│ - 계획이 항상 일관됨                         │
└───────────────┬──────────────────────────┘
                │ 참조
                ▼
┌──────────────────────────────────────────┐
│     Booking (애그리게이트 루트)              │
│------------------------------------------│
│ 식별자: bookingId                          │
│                                          │
│ 불변 조건:                                 │
│ - 상태가 제공자 상태를 반영                    │
│ - 취소 규칙이 강제됨                          │
└───────────────┬──────────────────────────┘
                │ 의존
                ▼
┌──────────────────────────────────────────┐
│     Payment (애그리게이트 루트)               │
│------------------------------------------│
│ 식별자: paymentId                          │
│                                          │
│ 불변 조건:                                 │
│ - 한 번만 청구                              │
│ - 정확히 환불                               │
└──────────────────────────────────────────┘

다음 편에서는 도메인 객체의 생성을 다루는 팩토리(Factory)를 살펴볼게요. 기존 객체의 접근을 관리하는 리포지토리(Repository)를 살펴볼게요.

도메인 주도 설계 시리즈