본문으로 건너뛰기
Leo Jung
뒤로 가기

[DDD Intro #5] Aggregate: 일관성의 경계

페이지 수정

DDD의 전술적 패턴 중에서 가장 중요하면서도 가장 많이 오해되는 개념이 Aggregate다. Entity와 Value Object는 비교적 직관적으로 받아들이기 쉽다. 하지만 Aggregate는 처음 접하면 애매하다.

관련 있는 객체들을 묶는 것일까? 트랜잭션 단위일까? ORM에서 연관관계로 로딩되는 객체 그래프일까? Aggregate Root는 단순히 대표 Entity일까?

Aggregate를 제대로 이해하려면 먼저 “무엇을 묶을 것인가”가 아니라 “무엇을 일관되게 지켜야 하는가”를 물어야 한다.

Aggregate는 객체를 예쁘게 묶는 단위가 아니다. Aggregate는 비즈니스 불변식을 보호하는 일관성의 경계다.

불변식이 먼저다

불변식은 시스템이 항상 지켜야 하는 비즈니스 규칙이다. 단순한 입력 검증과는 다르다. 값이 null이 아니어야 한다는 것도 중요하지만, DDD에서 Aggregate를 고민할 때 더 중요한 것은 도메인적으로 깨지면 안 되는 규칙이다.

구독 서비스를 예로 들어보자.

활성 구독은 반드시 현재 이용 기간을 가져야 한다.
만료된 구독은 콘텐츠 접근 권한을 부여하면 안 된다.
무료 체험은 한 사용자에게 한 번만 제공된다.
결제 실패 횟수가 일정 기준을 넘으면 구독은 일시 중지된다.
해지 예약된 구독은 현재 기간이 끝날 때 만료된다.

이런 규칙 중 어떤 것은 하나의 Aggregate 안에서 즉시 일관성을 보장해야 한다. 어떤 것은 다른 Aggregate나 다른 컨텍스트와 협력하며 최종적으로 일관되어도 된다.

Aggregate 설계의 핵심은 이 구분이다.

무엇이 반드시 같은 트랜잭션 안에서 함께 변경되어야 하는가?
무엇은 이벤트를 통해 나중에 반영되어도 되는가?
어떤 규칙이 깨지면 도메인적으로 치명적인가?

이 질문 없이 “구독과 결제는 관련 있으니 같은 Aggregate에 넣자”라고 판단하면 Aggregate는 쉽게 커진다.

Aggregate는 관계가 아니라 변경의 경계다

많은 사람이 Aggregate를 객체 관계 중심으로 이해한다.

구독에는 요금제가 있고, 결제 정보가 있고, 이용 권한이 있고, 쿠폰이 있고, 환불 이력이 있으니 전부 Subscription Aggregate 안에 넣어야 한다고 생각한다.

Subscription
- Plan
- PaymentMethod
- Payments
- Entitlements
- Coupons
- Refunds
- SupportTickets

현실에서는 이들이 모두 관련되어 있다. 하지만 관련이 있다는 이유만으로 같은 Aggregate가 되어야 하는 것은 아니다.

Aggregate는 “함께 조회하면 편한 것”의 경계가 아니다. “서로 관련 있는 것”의 경계도 아니다. Aggregate는 “함께 일관성을 지켜야 하는 변경”의 경계다.

구독과 결제는 밀접하게 관련되어 있지만, 항상 같은 Aggregate 안에 있어야 하는 것은 아니다. 결제는 실패할 수 있고, 재시도될 수 있으며, 환불될 수 있다. 결제 이력은 많아질 수 있다. 결제 정책은 구독 정책과 다른 속도로 변할 수 있다.

이 경우 SubscriptionPayment를 별도의 Aggregate로 두고, 결제 성공이나 실패를 이벤트로 연결하는 편이 더 나을 수 있다.

Payment Aggregate
  -> PaymentCompleted 이벤트 발행

Subscription Aggregate
  -> 결제 완료 이벤트를 기반으로 갱신 처리

물론 모든 시스템에서 이렇게 해야 한다는 뜻은 아니다. 중요한 것은 관계가 아니라 일관성 기준으로 판단해야 한다는 점이다.

Aggregate Root의 역할

Aggregate에는 외부에서 접근할 수 있는 진입점이 필요하다. 이것이 Aggregate Root다.

Aggregate Root는 단순히 ID를 가진 대표 객체가 아니다. Aggregate Root는 Aggregate 내부의 일관성을 책임지는 객체다. 외부 객체는 Aggregate 내부 Entity나 Value Object를 직접 변경해서는 안 된다. 모든 변경은 Root를 통해 이루어져야 한다.

예를 들어 Subscription Aggregate 안에 SubscriptionPeriodRenewalPolicy 같은 값 객체가 있다고 해보자. 외부 코드가 이 값을 직접 수정하면 Aggregate의 규칙을 우회할 수 있다.

나쁜 흐름은 이런 식이다.

subscription.getCurrentPeriod().extendByOneMonth();
subscription.setStatus(ACTIVE);

이 코드는 구독 기간이 왜 연장되었는지, 연장 가능한 상태였는지, 어떤 이벤트가 발생해야 하는지 숨긴다.

더 나은 흐름은 도메인 행위를 Root에 요청하는 것이다.

subscription.renew(paymentResult);

이제 Subscription은 자신의 상태를 확인하고, 기간을 갱신하고, 필요한 이벤트를 기록할 수 있다. 외부 코드는 내부 구조를 알 필요가 없다.

Aggregate Root는 내부 객체들을 감싸는 껍데기가 아니라, 도메인 규칙이 우회되지 않도록 지키는 문이다.

작게 설계해야 하는 이유

Aggregate는 작게 설계하는 것이 좋다. 이유는 단순하다. Aggregate가 커질수록 변경 비용이 커지고, 트랜잭션 충돌이 많아지고, 도메인 책임이 흐려지기 때문이다.

큰 Aggregate는 처음에는 편하다. 한 번 조회하면 모든 데이터가 있고, 한 트랜잭션 안에서 많은 것을 처리할 수 있다. 하지만 시간이 지나면 문제가 드러난다.

첫째, 성능 문제가 생긴다. 작은 변경을 위해 거대한 객체 그래프를 로딩하게 된다.

둘째, 동시성 문제가 생긴다. 서로 다른 사용자가 서로 다른 부분을 변경하는데 같은 Aggregate 버전을 두고 충돌할 수 있다.

셋째, 책임이 섞인다. 구독 갱신, 결제 실패, 환불, 고객센터 보상, 이용 권한 조정이 모두 하나의 Aggregate에 들어가면 모델이 무거워진다.

넷째, 경계가 흐려진다. 무엇이 구독의 책임이고 무엇이 결제의 책임인지 알기 어려워진다.

Aggregate는 크게 만들기는 쉽지만, 나중에 쪼개기는 어렵다. 그래서 처음에는 진짜 불변식을 중심으로 작게 시작하는 편이 좋다.

다른 Aggregate는 ID로 참조한다

DDD 구현에서 자주 언급되는 원칙이 있다. Aggregate는 다른 Aggregate를 객체 참조가 아니라 ID로 참조하는 것이 좋다.

예를 들어 SubscriptionSubscriber Aggregate를 직접 들고 있다고 해보자.

public class Subscription {
    private Subscriber subscriber;
}

이 구조는 편해 보인다. 하지만 Subscription을 로딩할 때 Subscriber도 함께 로딩되고, Subscriber의 변경까지 같은 객체 그래프 안에서 다뤄질 수 있다. 두 Aggregate의 경계가 흐려진다.

대신 ID로 참조하면 경계가 더 선명해진다.

public class Subscription {
    private SubscriberId subscriberId;
}

이것은 관계를 끊는 것이 아니다. 참조 방식으로 경계를 지키는 것이다. Subscription은 구독 규칙에 집중하고, Subscriber는 구독자의 규칙에 집중한다. 필요하면 Application Service가 두 Aggregate를 조회해 유스케이스를 조율할 수 있다.

ID 참조는 불편함을 만들기도 한다. 하지만 그 불편함이 오히려 중요한 신호가 될 수 있다. 두 Aggregate를 항상 함께 변경해야 한다면 경계를 다시 검토해야 한다. 반대로 함께 변경할 필요가 없다면 ID 참조가 모델을 더 건강하게 유지한다.

Application Service와 Aggregate의 협력

Aggregate가 모든 일을 혼자 해야 하는 것은 아니다. Application Service는 유스케이스 흐름을 조율한다.

예를 들어 구독 갱신 흐름을 보자.

public void renewSubscription(SubscriptionId subscriptionId) {
    Subscription subscription = subscriptionRepository.findById(subscriptionId);
    PaymentResult paymentResult = paymentClient.pay(subscription.billingAmount());

    subscription.renew(paymentResult);

    subscriptionRepository.save(subscription);
}

이 예시에서 Application Service는 구독을 조회하고, 외부 결제 시스템을 호출하고, 도메인 객체에 갱신을 요청하고, 변경을 저장한다. 하지만 갱신 가능한 상태인지, 결제 성공 시 기간을 어떻게 늘릴지, 결제 실패 시 어떤 상태로 전환할지는 Subscription이 결정한다.

나쁜 구조는 Application Service가 모든 도메인 규칙을 판단하는 것이다.

if (subscription.getStatus() == ACTIVE && paymentResult.isSuccess()) {
    subscription.setPeriod(nextPeriod);
    subscription.setStatus(ACTIVE);
} else if (paymentResult.isFailed()) {
    subscription.setFailureCount(subscription.getFailureCount() + 1);
    if (subscription.getFailureCount() >= 3) {
        subscription.setStatus(SUSPENDED);
    }
}

이렇게 되면 Aggregate는 데이터 묶음이 되고, 도메인 규칙은 Application Service에 흩어진다. DDD에서 Aggregate는 행위를 가져야 한다. 자신이 지켜야 하는 불변식을 스스로 보호해야 한다.

Aggregate와 트랜잭션

Aggregate는 트랜잭션 경계와 깊게 연결된다. 하나의 Aggregate를 변경하는 작업은 보통 하나의 트랜잭션 안에서 처리한다. Aggregate 내부의 불변식은 즉시 일관성을 보장한다.

반대로 여러 Aggregate를 한 트랜잭션 안에서 동시에 변경하려는 요구가 많다면 주의해야 한다. 정말 반드시 즉시 일관성이 필요한가? 아니면 도메인 이벤트를 통해 최종적 일관성으로 처리해도 되는가?

예를 들어 결제가 완료되면 구독 기간이 갱신되어야 한다. 이 작업을 반드시 한 트랜잭션으로 묶어야 할 수도 있다. 하지만 결제 이력 저장, 이메일 발송, 마케팅 분석, 추천 모델 업데이트까지 모두 같은 트랜잭션 안에 넣을 필요는 없다.

이벤트를 통해 후속 작업으로 분리할 수 있다.

SubscriptionRenewed
- 이용 권한 갱신
- 영수증 이메일 발송
- 마케팅 분석 반영

Aggregate는 모든 결과를 즉시 완성하려는 구조가 아니다. 반드시 지켜야 할 일관성과 나중에 맞춰도 되는 일관성을 구분하는 구조다.

Aggregate 설계 질문

Aggregate를 설계할 때는 다음 질문들이 도움이 된다.

이 Aggregate가 보호해야 하는 불변식은 무엇인가?
어떤 변경이 반드시 한 트랜잭션 안에서 일어나야 하는가?
외부에서 직접 바꾸면 안 되는 내부 상태는 무엇인가?
다른 Aggregate와의 관계는 ID로 충분한가?
이 Aggregate가 너무 많은 유스케이스를 책임지고 있지는 않은가?
조회 편의를 위해 Aggregate를 키우고 있지는 않은가?

특히 마지막 질문이 중요하다. 조회 화면에 여러 정보가 함께 나온다고 해서 그것이 같은 Aggregate여야 하는 것은 아니다. 조회 요구사항은 별도의 읽기 모델이나 쿼리로 해결할 수 있다. Aggregate는 조회 편의가 아니라 변경 일관성을 기준으로 설계해야 한다.

마치며

Aggregate는 DDD 구현의 핵심이다. 하지만 관련 객체를 모두 묶는 단위로 이해하면 쉽게 실패한다. Aggregate는 객체 그래프가 아니다. ORM의 연관관계도 아니다. 화면에 함께 보이는 데이터 묶음도 아니다.

Aggregate는 일관성의 경계다. 비즈니스 불변식을 보호하고, 외부 코드가 내부 규칙을 우회하지 못하게 하며, 어떤 변경을 한 트랜잭션 안에서 책임질지 결정한다.

좋은 Aggregate는 작고 명확하다. 자신이 지켜야 할 규칙을 알고, 외부에는 의미 있는 행위를 제공하며, 다른 Aggregate와는 느슨하게 협력한다.

다음 글에서는 Aggregate 안에서 일어난 중요한 변화를 어떻게 표현하고 다른 관심사와 연결할 수 있는지 살펴본다. 그 개념이 Domain Event다.


페이지 수정
공유하기:

이전 글
[DDD Intro #6] Domain Event: 도메인에서 일어난 일을 표현하기
다음 글
[DDD Intro #4] Bounded Context: 모델은 경계 안에서만 일관된다