Aggregate

정의

Aggregate는 하나의 단위로 취급할 수 있는 도메인 객체의 묶음이다. 주문과 주문 항목이 전형적인 예다. 이들은 별개의 객체지만 주문(과 그 항목들)을 하나의 Aggregate로 다루는 것이 유용하다.

Fowler가 정리한 핵심 속성은 셋이다.

  • Aggregate의 구성 객체 중 하나가 Aggregate Root가 된다. 외부에서 오는 참조는 Root만 향해야 하며, 그래야 Root가 Aggregate 전체의 정합성을 보장할 수 있다
  • Aggregate는 데이터 저장의 기본 전송 단위다. 로드와 저장은 Aggregate 전체 단위로 요청한다
  • 트랜잭션은 Aggregate 경계를 넘지 않아야 한다

DDD의 Aggregate는 리스트나 맵 같은 컬렉션 클래스와 혼동되곤 한다. 컬렉션은 범용 구조이고, DDD Aggregate는 주문·진료 방문·재생목록 같은 도메인 개념이다. 하나의 Aggregate가 여러 컬렉션과 단순 필드를 함께 가지는 경우가 많다. UML 등 다른 맥락에서 쓰이는 “aggregate"는 DDD Aggregate와 같은 개념이 아니다.


해결하는 문제

Evans가 이 패턴을 도입한 문제 상황은 다음과 같다.

복잡한 연관관계를 가진 모델에서 객체 변경의 일관성을 보장하는 것은 어렵다. 객체는 자신의 내부 상태를 일관되게 유지해야 하지만, 개념적으로 구성 부분에 해당하는 다른 객체의 변경에 무방비로 노출될 수 있다. 조심스러운 DB 락 전략은 여러 사용자가 무의미하게 서로를 방해하게 만들어 시스템을 사용 불가능하게 만들 수 있다. 객체를 여러 서버에 분산하거나 비동기 트랜잭션을 설계할 때도 비슷한 문제가 발생한다.

처방은 이렇다.

Entity와 Value Object를 Aggregate로 묶고 각각의 경계를 정의하라. 각 Aggregate마다 하나의 Entity를 Root로 선택하고, 외부 객체는 Root에 대한 참조만 갖게 하라(내부 구성원에 대한 참조는 단일 연산 내에서 쓰이도록 전달되는 경우만 허용). Aggregate 전체에 대한 속성과 불변식을 정의하고, 그 강제 책임을 Root나 지정된 프레임워크 장치에 부여하라.

같은 Aggregate 경계를 트랜잭션과 분산을 지배하는 데 사용하라. 경계 안에서는 일관성 규칙을 동기적으로 적용하고, 경계를 넘어서는 갱신은 비동기로 처리하라. Aggregate 하나는 한 서버에 함께 두고, 서로 다른 Aggregate는 노드에 분산될 수 있게 하라.


4가지 설계 규칙

Vaughn Vernon의 Effective Aggregate Design(2011, 3부작)이 Aggregate 크기 결정의 정본으로 통한다. 3부에 걸친 논의는 네 가지 규칙으로 요약된다.

  1. 진짜 불변식을 일관성 경계 안에 모델링한다
  2. Aggregate를 작게 설계한다
  3. 다른 Aggregate는 식별자로 참조한다
  4. 경계 밖에서는 결과적 일관성을 사용한다 (누구의 일인지 물어본 뒤에)

Vernon은 이 패턴이 “DDD 전술적 지침 중 가장 이해가 덜 된 것"이라고 진단한다. Aggregate를 잘못 모델링하는 방식은 양극단으로 나타난다. 구성의 편의를 위해 설계해 너무 크게 만드는 것과, 반대로 모든 Aggregate를 앙상하게 벗겨내 진짜 불변식을 보호하지 못하는 것이다.


규칙 1 - 진짜 불변식을 일관성 경계 안에 모델링한다

Bounded Context 안에서 Aggregate를 발견하려면 그 모델의 진짜 불변식을 이해해야 한다. 그 지식이 있어야만 어떤 객체들이 하나의 Aggregate로 묶여야 하는지 결정할 수 있다.

불변식(invariant)은 항상 일관되어야 하는 비즈니스 규칙이다. 일관성에는 여러 종류가 있는데, 하나는 즉각적이고 원자적인 트랜잭션 일관성이고 다른 하나는 결과적 일관성이다. 불변식을 논할 때 가리키는 것은 트랜잭션 일관성이다.

Vernon의 예시는 단순하다.

불변식:  c = a + b

a=2, b=3 이면 c는 반드시 5여야 한다.
c가 5가 아니면 시스템 불변식이 깨진다.

c를 일관되게 유지하려면 이 속성들 주위에 경계를 모델링한다.

AggregateType1 {
    int a; int b; int c;
    operations...
}

일관성 경계는 그 안의 모든 것이 어떤 연산을 수행하든 특정한 비즈니스 불변 규칙 집합을 준수한다고 논리적으로 주장한다. 경계 밖의 일관성은 그 Aggregate와 무관하다. 따라서 다음이 성립한다.

Aggregate는 트랜잭션 일관성 경계와 동의어다.

여기서 Vernon이 도출하는 두 문장이 실무에서 중요하다.

  • 잘 설계된 Aggregate는 비즈니스가 요구하는 어떤 방식으로 변경되어도 단일 트랜잭션 안에서 불변식이 완전히 일관되게 유지되는 Aggregate다
  • 잘 설계된 Bounded Context는 모든 경우에 트랜잭션당 하나의 Aggregate 인스턴스만 수정한다

그리고 결정적으로, 트랜잭션 분석 없이는 Aggregate 설계를 올바르게 추론할 수 없다.

트랜잭션당 하나로 제한하는 것은 지나치게 엄격해 보이지만 이것은 경험 법칙이며 대부분의 경우 목표로 삼아야 한다. Aggregate를 사용하는 바로 그 이유를 다루기 때문이다. 이 지침의 부수 효과로 UI 설계에도 영향이 간다. 각 요청이 하나의 Aggregate 인스턴스에 하나의 커맨드를 실행하도록 집중해야 하며, 사용자 요청이 너무 많은 것을 달성하려 하면 애플리케이션이 여러 인스턴스를 한 번에 수정하도록 강요된다.

Aggregate는 주로 일관성 경계에 관한 것이며, 객체 그래프를 설계하고 싶은 욕구가 그것을 이끌지 않는다.


규칙 2 - Aggregate를 작게 설계한다

큰 Aggregate가 실패하는 방식

Vernon은 Scrum 관리 애플리케이션(ProjectOvation)을 사례로 든다. 팀은 보편 언어의 “Products have backlog items, releases, and sprints"에서 “have"에 무게를 두고, 객체 그래프처럼 상호 연결되어야 한다고 판단했다. 그 결과 Product가 모든 BacklogItem, Release, Sprint 인스턴스를 보유하는 아주 큰 Aggregate가 되었다.

public class Product extends ConcurrencySafeEntity {
    private Set<BacklogItem> backlogItems;
    private String description;
    private String name;
    private ProductId productId;
    private Set<Release> releases;
    private Set<Sprint> sprints;
    private TenantId tenantId;
    ...
}

이 설계는 다중 사용자 환경에서 실행되자 트랜잭션 실패를 상시적으로 겪기 시작했다. Aggregate 인스턴스가 낙관적 동시성으로 보호되고 있었기 때문이다.

1. 사용자 Bill과 Joe가 같은 Product(version 1)를 조회하고 작업을 시작한다.
2. Bill이 새 BacklogItem을 계획하고 커밋한다.  → Product version이 2로 증가
3. Joe가 새 Release를 예약하고 저장을 시도한다. → version 1 기반이므로 커밋 실패

새 백로그 항목을 계획하는 일이 새 릴리스를 예약하는 일을 방해할 논리적 이유는 전혀 없다. Joe의 커밋이 실패한 근본 원인은 이 큰 Aggregate가 진짜 비즈니스 규칙이 아니라 거짓 불변식(false invariant)을 염두에 두고 설계되었다는 점이다. 거짓 불변식은 개발자가 부과한 인위적 제약이다. 사용자가 둘뿐일 때도 이 문제가 나타났고, 사용자가 늘어나면 심각해진다.

성능과 확장성

트랜잭션 실패를 없앤다 해도(예: Hibernate의 optimistic-lockfalse로) 문제가 끝나지 않는다.

수년 된 제품에 이미 수천 개의 백로그 항목이 있는 상황에서 사용자가 백로그 항목 하나를 추가하려 한다고 하자. 지연 로딩을 지원하는 영속성 장치를 가정해도, 이미 큰 컬렉션에 새 원소 하나를 추가하기 위해 수천 개의 백로그 항목이 메모리로 로딩된다. 지연 로딩을 지원하지 않으면 더 나쁘다. 백로그 항목을 릴리스에 예약하거나 스프린트에 커밋할 때는 여러 컬렉션을 동시에 로딩해야 한다.

이 큰 Aggregate는 결코 잘 동작하거나 확장되지 않는다. 거짓 불변식과 구성의 편의에 대한 욕구가 설계를 이끌면서 트랜잭션 성공·성능·확장성을 모두 훼손한 것이다.

“작다"의 기준

극단은 전역 식별자와 속성 하나만 가진 Aggregate지만 그것이 권고 사항은 아니다. 기준은 이렇다.

Aggregate를 Root Entity와 최소한의 속성 또는 값 타입 프로퍼티로 제한한다. 올바른 최소값은 필요한 것들, 그리고 그 이상은 아니다.

무엇이 필요한가에 대한 답은 다른 것들과 일관되어야 하는 것들이다. 도메인 전문가가 규칙으로 명시하지 않아도 마찬가지다. 예를 들어 Productnamedescription은 서로 불일치하는 상태를 상상하기 어렵다. 하나를 바꿀 때 보통 다른 것도 바꾸며, 하나만 바꾼다면 오타 수정이거나 설명을 이름에 더 맞게 다듬는 경우다. 도메인 전문가가 명시적 규칙으로 생각하지 않더라도 이것은 암묵적 불변식이다.

Entity보다 Value Object를 선호한다

포함된 부분을 Entity로 모델링해야 할 것 같다면, 먼저 그 부분이 시간에 따라 스스로 변해야 하는지, 아니면 변경이 필요할 때 완전히 교체될 수 있는지 물어본다. 완전히 교체될 수 있다면 Entity보다 Value Object를 가리킨다. 사례별로 이 점검을 거치면 Entity로 모델링된 많은 개념을 Value Object로 리팩터링할 수 있다.

Value 타입을 Aggregate 부분으로 선호하는 것이 Aggregate를 불변으로 만드는 것을 뜻하지는 않는다. 값 타입 프로퍼티가 교체될 때 Root Entity 자체는 변한다.

내부 부분을 값으로 제한하는 이점은 다음과 같다.

  • 영속성 장치에 따라 값은 Root Entity와 함께 직렬화될 수 있다. Entity는 보통 별도 추적 저장이 필요하다
  • Entity 부분은 오버헤드가 크다. Hibernate로 읽을 때 SQL 조인이 필요한 경우가 그렇다. DB 테이블 한 행을 읽는 것이 훨씬 빠르다
  • Value Object가 더 작고 안전하며(버그가 적다), 불변성 덕분에 단위 테스트로 정확성을 증명하기 쉽다

Vernon이 인용하는 실측 사례가 있다. 금융 파생상품 분야의 한 프로젝트에서 Niclas Hedhman의 팀은 전체 Aggregate의 약 70%를 Root Entity 하나와 값 타입 프로퍼티만으로 설계할 수 있었고, 나머지 30%도 총 2~3개의 Entity만 가졌다. 모든 도메인 모델이 70/30으로 갈린다는 뜻은 아니지만, 높은 비율의 Aggregate를 Root 단일 Entity로 제한할 수 있다는 것을 보여준다.

작은 Aggregate는 성능과 확장성이 좋을 뿐 아니라 트랜잭션 성공에 편향되어 커밋을 막는 충돌이 드물어진다.


규칙 3 - 다른 Aggregate는 식별자로 참조한다

Evans의 원서는 한 Aggregate가 다른 Aggregate의 Root에 대한 참조를 가질 수 있다고 말한다. 그러나 그 참조가 참조된 Aggregate를 참조하는 쪽의 일관성 경계 안으로 끌어들이지는 않는다. 참조가 하나의 온전한 Aggregate를 형성시키지 않으며, 여전히 둘(또는 그 이상)이다.

// 직접 객체 참조
public class BacklogItem extends ConcurrencySafeEntity {
    private Product product;
}

이 형태의 함의는 셋이다.

  1. 참조하는 Aggregate(BacklogItem)와 참조된 Aggregate(Product)는 같은 트랜잭션에서 수정되어서는 안 된다. 한 트랜잭션에서는 어느 한쪽만 수정할 수 있다
  2. 여러 인스턴스를 한 트랜잭션에서 수정하고 있다면 일관성 경계가 틀렸다는 강한 신호일 수 있다. 아직 발견하지 못한 보편 언어의 개념이 손을 흔들며 소리치고 있는 것일 수 있다
  3. 2번을 적용하려는데 그것이 큰 Aggregate를 만들게 된다면, 원자적 일관성 대신 결과적 일관성을 써야 한다는 신호일 수 있다

처방은 다음과 같다.

외부 Aggregate에 대한 참조는 전역 고유 식별자로만 하는 것을 선호하고, 직접 객체 참조(포인터)를 보유하지 않는다.

// 식별자 참조
public class BacklogItem extends ConcurrencySafeEntity {
    private ProductId productId;
}

추론된 객체 참조를 가진 Aggregate는 자동으로 더 작아진다. 참조가 결코 즉시 로딩되지 않기 때문이다. 인스턴스 로딩 시간이 줄고 메모리를 덜 쓰므로 모델의 성능이 개선되고, 메모리 할당 오버헤드와 가비지 컬렉션에도 긍정적이다.

모델 탐색

식별자 참조가 모델 탐색을 완전히 막지는 않는다. Aggregate 내부에서 Repository를 써서 조회하는 기법이 있는데(disconnected domain model, 사실상 지연 로딩의 한 형태), Vernon은 다른 접근을 권한다.

Aggregate 행위를 호출하기 전에 Repository나 Domain Service로 의존 객체를 미리 조회한다. 클라이언트 Application Service가 이를 제어한 뒤 Aggregate로 위임한다.

public class ProductBacklogItemService ... {
    @Transactional
    public void assignTeamMemberToTask(
        String aTenantId, String aBacklogItemId,
        String aTaskId, String aTeamMemberId) {

        BacklogItem backlogItem =
            backlogItemRepository.backlogItemOfId(
                new TenantId(aTenantId), new BacklogItemId(aBacklogItemId));

        Team ofTeam =
            teamRepository.teamOfId(backlogItem.tenantId(), backlogItem.teamId());

        backlogItem.assignTeamMemberToTask(
            new TeamMemberId(aTeamMemberId), ofTeam, new TaskId(aTaskId));
    }
}

Application Service가 의존성을 해결하면 Aggregate는 Repository나 Domain Service에 의존하지 않아도 된다. 다만 한 요청에서 여러 Aggregate를 참조하는 것이 그중 둘 이상을 수정할 면허를 주지는 않는다.

식별자 참조만 쓰면 UI 뷰를 조립하는 클라이언트를 대응하기가 어려워질 수 있다. 하나의 유스케이스에서 여러 Repository를 써야 하고, 쿼리 오버헤드가 성능 문제를 일으키면 CQRS 도입을 고려할 가치가 있다. 또는 추론된 참조와 직접 참조 사이에서 균형을 잡아야 할 수도 있다.

확장성과 분산

Aggregate가 직접 참조 대신 식별자 참조를 쓰기 때문에 영속 상태를 옮길 수 있고, 그래서 큰 규모에 도달할 수 있다. Aggregate 데이터 저장을 계속 재분할하는 것이 가능해지기 때문이다.

분산은 저장소를 넘어서도 적용된다. 이벤트 기반 접근에서는 Aggregate 식별자를 담은 메시지 기반 Domain Event가 조직 전체에 전파되고, 외부 Bounded Context의 구독자가 그 식별자로 자기 도메인 모델에서 연산을 수행한다. 식별자 참조가 원격 연관을 형성하는 것이다. 분산 시스템 간 트랜잭션은 원자적이지 않으므로, 여러 시스템이 여러 Aggregate를 결과적으로 일관된 상태로 만든다.

ORM에서 @ManyToOne 같은 연관을 촘촘히 걸면 서비스를 분리할 때 모든 엔티티가 서로를 참조해 자를 지점이 없어진다. 식별자 참조로 짜두면 그 전환이 “리포지토리 호출 한 줄이 원격 호출 한 줄로” 바뀌는 것으로 끝난다. 코드에서 객체 참조를 하지 않는 것과 DB가 FK로 정합성을 보장하는 것은 별개 문제이므로, 단일 DB 단계에서 FK를 유지하는 것은 이 규칙과 충돌하지 않는다.


규칙 4 - 경계 밖에서는 결과적 일관성을 사용한다

Evans 원서 128쪽의 문장이 근거다.

Aggregate를 넘나드는 규칙은 항상 최신 상태일 것으로 기대되지 않는다. 이벤트 처리, 배치 처리, 또는 다른 갱신 장치를 통해 다른 의존성들이 특정 시간 내에 해소될 수 있다.

따라서 한 Aggregate 인스턴스에 커맨드를 실행하는 것이 하나 이상의 다른 Aggregate에서 추가 비즈니스 규칙이 실행되어야 함을 요구한다면, 결과적 일관성을 사용한다.

구현 방법은 Aggregate 커맨드 메서드가 Domain Event를 발행하고, 그것이 비동기 구독자에게 전달되는 것이다.

public class BacklogItem extends ConcurrencySafeEntity {
    public void commitTo(Sprint aSprint) {
        ...
        DomainEventPublisher.instance().publish(
            new BacklogItemCommitted(
                this.tenantId(), this.backlogItemId(), this.sprintId()));
    }
}

각 구독자는 서로 다른 Aggregate 인스턴스를 조회해 자기 행위를 실행한다. 각 구독자는 별도 트랜잭션에서 실행되므로 “트랜잭션당 하나의 인스턴스” 규칙을 지킨다.

구독자가 다른 클라이언트와 동시성 경합을 겪어 실패하면, 메시징 장치에 성공을 응답하지 않는 방식으로 재시도할 수 있다. 메시지가 재전달되고 새 트랜잭션이 시작되어 다시 시도된다. 이 재시도는 일관성이 달성되거나 재시도 한계에 도달할 때까지 계속될 수 있다. 완전히 실패하면 보상 처리가 필요하거나, 최소한 개입을 위해 실패를 보고해야 한다.

누구의 일인지 물어본다

트랜잭션 일관성과 결과적 일관성 중 무엇을 쓸지 판단하기 어려운 시나리오가 있다. 전통적 DDD 사용자는 트랜잭션 일관성으로 기울고 CQRS 사용자는 결과적 일관성으로 기울지만, 둘 다 도메인에 근거한 답이 아니라 기술적 선호일 뿐이다.

Vernon이 Evans와 논의해 얻은 판별법은 단순하다.

유스케이스를 검토할 때, 데이터를 일관되게 만드는 것이 그 유스케이스를 실행하는 사용자의 일인지 물어본다.

  • 그 사용자의 일이라면, Aggregate의 다른 규칙을 지키는 범위에서 트랜잭션 일관성을 시도한다
  • 다른 사용자의 일이거나 시스템의 일이라면 결과적 일관성을 허용한다

이 판별법은 편리한 결정 기준이면서 도메인을 더 깊이 이해하게 해준다. 유지되어야 하는 진짜 시스템 불변식을 드러내기 때문이다.

도메인 전문가는 개발자보다 지연 일관성에 훨씬 편안한 경우가 많다. 업무에서 상시적으로 발생하는 현실적 지연을 알고 있기 때문이다. 개발자는 보통 원자적 변경 사고방식에 익숙하다. 도메인 전문가에게 한 인스턴스의 수정과 나머지 사이에 얼마간의 시간 지연을 감내할 수 있는지 물어보는 것이 좋다.


규칙을 어길 만한 이유

Vernon은 경험 있는 실무자가 정당한 이유로 여러 Aggregate 인스턴스를 한 트랜잭션에서 영속화하기로 결정할 수 있다고 인정하며, 네 가지 이유를 든다.

이유내용
1. UI 편의성여러 항목의 공통 속성을 한 번에 정의해 일괄 생성하는 UI. 생성되는 것은 각자 불변식을 유지하는 온전한 Aggregate이므로, 하나씩 반복 생성하는 것과 의미상 다르지 않다면 규칙을 어겨도 무해하다
2. 기술 장치의 부재결과적 일관성은 메시징·타이머·백그라운드 스레드 같은 대역 외 처리 능력을 요구한다. 그런 장치가 전혀 없는 프로젝트라면 선택지가 좁아진다. 이때 user-aggregate affinity(특정 시점에 한 사용자만 특정 인스턴스 집합에 집중하는 업무 흐름)가 성립하면 결정이 더 안전해진다
3. 전역 트랜잭션레거시 기술이나 조직 정책상 2단계 커밋을 강제로 써야 하는 경우. 다만 전역 트랜잭션을 쓰더라도 자기 Bounded Context 안에서는 여러 인스턴스를 동시에 수정하지 않을 수 있다
4. 쿼리 성능Repository 쿼리 성능 문제를 완화하기 위해 직접 객체 참조를 보유하는 것이 나을 때가 있다. 크기와 성능 트레이드오프를 신중히 저울질해야 한다

Vernon의 결론은 다음과 같다. 규칙을 어길 핑계를 찾아다니지는 않는다. 장기적으로는 규칙을 지키는 것이 프로젝트에 이익이다.

규칙을 알고 어긴 것과 몰라서 어긴 것은 코드에서 구분되지 않는다. 구분되는 곳은 기록뿐이다. 의도적으로 어겼다면 그 트레이드오프와 갚을 시점을 ADR(Architecture Decision Record) 같은 형태로 남겨두는 것이 실무적이다.


사례 - 크기를 어림 계산으로 검증하기

Vernon의 3부는 Aggregate 크기를 BOTE(back-of-the-envelope) 계산으로 검증하는 과정을 보여준다. 규칙을 기계적으로 적용하는 대신 실제 비용을 추정하는 방식이다.

BacklogItemTask 컬렉션을 포함하고, 각 TaskEstimationLogEntry 컬렉션을 갖는 구조를 검토한다.

스프린트 길이       2~3주 → 12일로 가정
태스크당 시간       4~16시간 → 12시간으로 가정 (매일 1시간씩 재추정 → 태스크당 12개 로그)
백로그 항목당 태스크  계층별 2~3개 × 4계층 ≈ 11개 → 넉넉히 12개

총 수집 객체 = 12 태스크 × 12 로그 = 144개

144개는 많아 보이지만, 실제 사용 시나리오를 따져보면 다른 결론이 나온다. 지연 로딩을 쓰면 한 요청에서 메모리에 올라오는 것은 백로그 항목 1 + 태스크 12 + 로그 12 = 최대 25개다. 태스크 컬렉션 전체가 로딩되고, 그중 한 태스크의 로그 컬렉션이 로딩되기 때문이다. 그리고 이 상한은 스프린트 마지막 날에야 도달한다.

동시성 경합도 검토 대상이었다. 불변식은 “모든 태스크의 남은 시간이 0이 되면 백로그 항목 상태가 done으로 자동 전이"였고, Root의 버전은 상태가 실제로 바뀔 때만 증가한다. 분석 결과 144번의 재추정 중 상태를 전이시키는 것은 마지막 1번뿐이었다. 즉 143번은 Root를 건드리지 않는다.

이 분석에서 나오는 실무적 교훈은 다음과 같다.

  • 크기 판단은 개념적 소속이 아니라 실제 로딩되는 객체 수와 경합 빈도로 한다
  • “누구의 일인지” 질문을 던지자 새로운 도메인 통찰이 나왔다. 백로그 항목을 완료로 표시하는 것이 팀원의 일인지 제품 책임자의 일인지에 따라 트랜잭션 일관성이 필요할 수도, 아예 필요 없을 수도 있었다
  • 이 검토 전체는 30분에서 최대 60분이면 된다. 핵심 도메인에 대한 깊은 통찰을 얻는 대가로는 충분히 값싸다

Aggregate 크기 판단 체크리스트

  • 이 둘이 동시에 바뀌지 않으면 데이터가 잘못되는가. 아니라면 다른 Aggregate다
  • 지키려는 규칙이 도메인 전문가가 말한 진짜 불변식인가, 개발자가 부과한 거짓 불변식인가
  • 포함하려는 부분이 스스로 변해야 하는가, 완전히 교체 가능한가 (후자면 Value Object)
  • 한 요청에서 로딩되는 객체가 몇 개인가
  • 이 Aggregate에 동시에 쓰기를 시도하는 사용자가 실제로 여러 명인가
  • 일관성을 맞추는 것이 이 유스케이스를 실행하는 사용자의 일인가

예제 - 좌석 예매

공연 티켓 예매 시스템에서 지켜야 할 불변식이 “한 회차의 한 좌석은 동시에 최대 1개의 유효한 예약에만 속한다"라고 하자. Aggregate 대안은 셋이다.

내용판정
AShow가 좌석 20,000개를 소유좌석 1석을 잡으려고 20,000행을 로딩하고, 모든 예매가 Show 한 행에서 직렬화된다. Vernon의 대형 Aggregate 사례와 동일한 구조
BReservation이 좌석을 소유“이 좌석이 지금 비었는가"를 알려면 모든 예약을 뒤져야 한다
CShowSeat·Reservation·Payment·Show를 각각 독립 Aggregate로채택

C의 근거는 Aggregate가 DB 행 1개로 떨어지므로 불변식 강제가 “행 하나에 대한 원자적 갱신"으로 축소된다는 점이다. 조건부 UPDATE, 낙관적 락, 비관적 락, 분산락 등 모든 동시성 제어 전략이 이 형태의 변형이므로 전략을 비교할 수 있는 상태가 된다. 대안 A를 택하면 락 대상이 Show 한 행이 되어 회차 전체가 직렬화된다.

부수 효과로 경합 범위도 작아진다. A구역 1번석과 Z구역 99번석은 서로 막지 않는다.

이 사례의 상세한 전개는 블로그 글 좌석 예매 시스템에서 Aggregate 경계를 정한 과정에 정리해 두었다.


관련 개념


참고 자료