주문의 경우에는 일단 주문을 접수하고 재고가 있는지 확인하고 재고가 없으면 주문을 취소한 다음 고객에게 고지하면 됩니다. 하지만 송금의 경우를 생각해 봅시다. 돈을 보내고 돈이 있는지 확인하고 없으면 송금을 회수하는 건 말이 안 됩니다. 어떤 문제를 푸는지에 따라, 상황에 따라 선후관계가 강력하게 구속되지 않은 경우가 있고, 강력하게 구속되는 경우가 있습니다. 이를 각각 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):  # Order 자체가 애그리게이트
    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는 원칙적으로는 애그리게이트 루트만 리턴해야 합니다. 왜냐하면 애그리게이트 루트는 불변성을 보장하는 역할을 하기 때문입니다. 애그리게이트가 불변성을 보장해야 하는데, 애그리게이트를 구성하는 내부 엔티티를 리턴하는 메서드가 있다면 그 내부 엔티티의 상태가 마음대로 바뀌어버릴 수 있다는 위험성을 가진다는 말이 됩니다.

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

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

읽기 전용 객체를 반환하는 것도 방법입니다. 그렇다면 일관성을 지킬 수 있겠지요.

# 조회 전용 서비스에서는 예외적으로 허용
class OrderQueryService:
    def get_order_items(self, order_id: OrderId) -> List[OrderItemDto]:
        # ✅ 조회만 하는 경우는 OK (CQRS 패턴)
        pass

    def get_delivery_address(self, order_id: OrderId) -> DeliveryAddressDto:
        # ✅ DTO로 변환해서 리턴
        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. 여러 애그리게이트에 속하는 개념의 모델링