주문의 경우에는 일단 주문을 접수하고 재고가 있는지 확인하고 재고가 없으면 주문을 취소한 다음 고객에게 고지하면 됩니다. 하지만 송금의 경우를 생각해 봅시다. 돈을 보내고 돈이 있는지 확인하고 없으면 송금을 회수하는 건 말이 안 됩니다. 어떤 문제를 푸는지에 따라, 상황에 따라 선후관계가 강력하게 구속되지 않은 경우가 있고, 강력하게 구속되는 경우가 있습니다. 이를 각각 Eventual Consistency(아주 약간의 시차가 있더라도 그냥 결과만 같으면 됨), Strong Consistency(하나의 트랜젝션에서 처리되어야 함)라고 부릅니다. 애그리게이트는 논리적으로 강하게 연관된 객체들의 묶음(Aggregate)을 하나의 단위로 취급하여, 이 묶음의 데이터가 항상 유효하고 강하게 일관된 상태로만 변경되도록 강제하는 역할을 합니다.

예를 들어 밴드 관리 소프트웨어에서는 강한 일관성을 가지는 다음과 같은 불변조건들이 있을 것입니다.

이런 기본적인 규칙이 잠시라도 깨지면 비즈니스가 완전히 엉망이 됩니다. 유효하고 일관되지 않은 상태가 되는 것이지요.

1. 애그리게이트 표현 방법

애그리게이트 루트는 항상 엔티티입니다.

# 애그리게이트 루트는 반드시 엔티티
class Order(Entity):
    def __init__(self, order_id: OrderId):
        self.id = order_id

# 값 객체는 식별자가 없어서 애그리게이트 루트가 될 수 없음
class Money(ValueObject):
    pass

실제 구현 방식들

# 방식 1: 엔티티가 곧 애그리게이트 루트
class Order(Entity):
    def __init__(self):
        self._items: list[OrderItem] = []  # 내부 엔티티들
        self.delivery_address = None  # 값 객체들
# 방식 2: 베이스 클래스 활용
class AggregateRoot(Entity):
    def __init__(self):
        self._domain_events = []

class Order(AggregateRoot):  # 애그리게이트 루트임을 명시
    pass
# 방식 3: 마커 인터페이스/프로토콜
from abc import ABC
class Aggregate(ABC):
    pass

class Order(Entity, Aggregate):
    pass

2. Repository와 Aggregate Root

Repository는 원칙적으로는 애그리게이트 루트만 리턴해야 합니다. 왜냐하면 애그리게이트 루트는 불변성을 보장하는 역할을 하기 때문입니다. 애그리게이트가 불변성을 보장해야 하는데, 애그리게이트를 구성하는 내부 엔티티를 따로 반환받는다면 애그리게이트 입장에서는 내부 속성의 상태가 마음대로 바뀌는 것이기 때문입니다.

class OrderRepository:
    def find(self, order_id: OrderId) -> Order:
        # 애그리게이트 루트 리턴
        return order

    def save(self, order: Order):
        # 전체 애그리게이트 저장
        pass

이런 건 절대 안됩니다.

# 하지만 이건 안됨
class OrderService:
    def get_order_item_for_update(self, item_id: ItemId) -> OrderItem:
        # 수정 가능한 내부 엔티티 직접 노출 - 위험함
        pass

class Order(Entity):
    def __init__(self):
        self._items: List[OrderItem] = []
    
    # 내부 엔티티 직접 반환 - 위험함
    def get_item(self, item_id: OrderItemId) -> OrderItem:
        return next(item for item in self._items if item.id == item_id)

    # 전체 리스트 직접 노출 - 위험함
    def get_items(self) -> List[OrderItem]:
        return self._items  # 외부에서 직접 수정 가능

3. 여러 애그리게이트에 속하는 개념의 모델링