앞 장에서 우리는 Catalog, Cart, Order, Payment, Admin, Support Assistant라는 여섯 개 핵심 컨텍스트를 나눴습니다. 이제 각 컨텍스트 안으로 더 들어가야 합니다. 컨텍스트가 도시 구획이라면, 애그리거트와 엔티티와 값 객체는 그 안의 건물과 방과 규칙에 가깝습니다.
DDD를 처음 적용할 때 가장 많이 생기는 오해는 모든 도메인 개념을 곧바로 테이블이나 클래스 하나로 바라보는 것입니다. 하지만 도메인 모델은 저장 구조보다 책임 구조가 먼저입니다. 무엇이 상태 변화의 중심이고, 무엇이 그 안에서 정체성을 가지며, 무엇은 단순 값으로 묶어야 하는지를 정리하지 않으면 코드가 금방 CRUD의 모음으로 무너집니다.
이 페이지의 목적은 이커머스 MVP에서 어떤 것을 애그리거트로 보고, 어떤 것을 엔티티로 두고, 어떤 것을 값 객체로 올릴지 분명히 하는 것입니다. 특히 Cart, Order, Payment는 상태 변화가 핵심이라 이 경계가 선명해야 이후 서비스 계층, API, 영속화 전략이 자연스럽게 나옵니다.
바운디드 컨텍스트만 잘 나눠도 어느 정도는 버틸 수 있습니다. 하지만 그 안에서 애그리거트가 잘못 잡히면 곧바로 문제가 생깁니다.
예를 들어:
이런 문제는 코드가 커질수록 더 심해집니다. 따라서 이번 단계에서는 "어떤 객체가 시스템의 진짜 주인인가"를 고르는 작업이 필요합니다.
이 페이지를 읽고 나면 아래 항목이 정리되어 있어야 합니다.
다음 장의 도메인 서비스/애플리케이션 서비스 경계도 이 기준 위에서 정해집니다.
먼저 세 용어를 짧게 정리합니다.
애그리거트는 하나의 일관성 경계입니다. 외부에서 상태를 바꿀 때는 보통 애그리거트 루트를 통해서만 접근해야 합니다.
즉 애그리거트는 "함께 지켜야 하는 규칙이 묶여 있는 단위"라고 보면 됩니다.
엔티티는 정체성이 중요한 객체입니다. 값이 조금 바뀌어도 같은 대상을 계속 같은 것으로 추적해야 합니다.
예:
값 객체는 정체성보다 의미가 중요한 객체입니다. 같은 값이면 같은 것으로 취급합니다.
예:
중요한 점은, 값 객체는 작다고 해서 값 객체가 아니라 "동등성 비교를 값으로 하는가"가 기준이라는 것입니다.
애그리거트를 잘 고르려면 아래 질문을 던져야 합니다.
이 기준으로 보면 이번 교재에서 핵심 애그리거트 후보는 아래와 같습니다.
CartOrderPaymentCatalog는 읽기 중심이어서 학습 단계에서는 상대적으로 단순하게 모델링할 수 있고, Admin과 Support Assistant는 자체 쓰기 애그리거트보다는 읽기 조합 관점이 더 중요합니다.
먼저 장바구니부터 보겠습니다.
장바구니는 사용자별 임시 구매 상태를 대표합니다. 여러 항목이 있어도 외부에서 직접 수정해야 하는 주인은 Cart 하나라고 보는 편이 자연스럽습니다.
Cart가 애그리거트 루트인가즉 장바구니 항목만 따로 수정하게 두면 전체 규칙을 지키기 어려워집니다.
CartItemCartItem은 장바구니 안에서 독립적인 식별 맥락을 가질 수 있습니다. 예를 들어 어떤 상품이 몇 개 담겼는지, 옵션이 무엇인지, 현재 수량이 얼마인지 같은 상태를 가집니다.
하지만 중요한 점은 CartItem이 독립 애그리거트는 아니라는 것입니다. 외부는 Cart를 통해서만 CartItem을 수정해야 합니다.
QuantityMoneyProductSnapshot여기서 ProductSnapshot은 장바구니에 담길 당시의 상품명/가격 일부를 값처럼 담는 형태로 단순화할 수 있습니다. 이번 교재에서는 학습 목적상 이 방식이 실용적입니다.
이제 가장 중요한 Order를 봅니다.
주문은 거래 단위입니다. 주문 항목, 총액, 주문 상태, 생성 시점, 사용자와의 관계가 모두 이 애그리거트 안에서 의미를 가집니다.
Order가 애그리거트 루트인가즉 주문 항목 하나만 떼어 수정하는 방식은 위험합니다. 주문은 전체가 하나의 비즈니스 기록입니다.
OrderItemOrderItem은 주문 안에서 식별 가능한 항목입니다. 어떤 상품을 몇 개 주문했는지, 주문 당시 가격이 얼마였는지 같은 정보를 가집니다.
하지만 역시 OrderItem은 독립 애그리거트가 아닙니다. 외부는 Order를 통해서만 수정해야 합니다.
OrderStatusMoneyShippingAddressOrderNumberOrderStatus는 문자열로 대충 두기 쉬운데, 실제로는 값 객체처럼 다루는 것이 좋습니다. 그래야 허용 가능한 상태 전이를 명확히 생각할 수 있습니다.
예를 들어:
CREATEDPAYMENT_PENDINGPAIDPAYMENT_FAILED이 상태들은 단순 값이 아니라, 도메인 규칙과 연결된 의미 있는 값입니다.
결제는 이번 교재에서 모의 결제 흐름으로 구현하더라도, 별도 모델로 보는 습관이 중요합니다.
Payment는 결제 시도와 그 결과를 대표합니다.
이번 교재 수준에서는 Payment 자체를 애그리거트 루트로 두고, 복잡한 하위 엔티티는 최소화할 수 있습니다.
필요하다면:
PaymentAttempt같은 개념을 둘 수 있지만, MVP에서는 과도하게 복잡하게 갈 필요는 없습니다.
PaymentStatusMoneyPaymentMethodTransactionReferencePaymentStatus 역시 값 객체처럼 다루는 것이 좋습니다.
예를 들어:
REQUESTEDCONFIRMEDFAILEDCatalog는 상태 변경보다 읽기가 중요한 컨텍스트입니다. 따라서 이번 교재에서는 상대적으로 단순하게 가져가도 됩니다.
상품은 식별 가능한 엔티티입니다.
PriceCategoryNameProductStatus이번 교재에서는 재고를 별도 애그리거트로 분리하지 않고 단순화할 수 있지만, 실전에서는 재고가 별도 관심사가 될 수 있다는 점은 기억해야 합니다.
이 두 컨텍스트는 애그리거트 중심보다 읽기 모델 중심으로 보는 것이 더 자연스럽습니다.
운영자 화면은 여러 컨텍스트의 상태를 읽어와 재구성하는 역할이 큽니다. 즉 핵심 쓰기 애그리거트보다 조회 모델과 요약 모델이 중요합니다.
챗봇 역시 여러 컨텍스트의 읽기 정보를 가져와 응답을 만드는 편이 자연스럽습니다. 따라서 이번 교재에서는 별도 복잡한 애그리거트보다 응답 정책과 질의 경계가 더 중요합니다.
애그리거트를 설계할 때 가장 중요한 질문 중 하나는 "상태 전이가 어디서 일어나야 하는가"입니다.
예를 들면:
CartOrderPayment, 그리고 그 결과에 따른 Order 상태 갱신이 원칙이 필요한 이유는, 상태 변경 책임이 분산되면 나중에 규칙이 여기저기 흩어지기 때문입니다.
처음에는 문자열과 숫자로 다 처리해도 될 것처럼 보입니다. 하지만 값 객체를 쓰면 아래 장점이 있습니다.
예를 들어 단순 int 대신 Quantity, 단순 str 대신 OrderStatus, 단순 dict 대신 ShippingAddress처럼 생각하면 모델이 더 명확해집니다.
이번 교재에서 최소한 아래 정도는 문서로 정리해 두는 것이 좋습니다.
| 컨텍스트 | Aggregate Root | Entity | Value Object |
|---|---|---|---|
| Catalog | Product 또는 단순 Product Model | Product | Price, CategoryName |
| Cart | Cart | CartItem | Quantity, Money, ProductSnapshot |
| Order | Order | OrderItem | OrderStatus, Money, ShippingAddress |
| Payment | Payment | 필요 시 PaymentAttempt | PaymentStatus, Money, PaymentMethod |
이 표는 뒤의 코드 설계에서 바로 참고할 수 있는 가장 실용적인 형태입니다.
이 다섯 가지 실수는 구현 단계에서 불필요한 결합과 규칙 누락으로 이어집니다.
이 페이지에서 우리는 각 컨텍스트 내부에서 무엇이 진짜 주인 객체인지, 어떤 것이 엔티티이고 어떤 것이 값 객체인지 정리했습니다. 이제 모델은 컨텍스트 수준뿐 아니라 내부 구조 수준에서도 훨씬 선명해졌습니다. 다음 페이지에서는 이 애그리거트들을 실제 사용 흐름에 연결하기 위해, 도메인 서비스와 애플리케이션 서비스의 경계를 정리합니다.
다음 페이지에서는 지금 정리한 애그리거트와 값 객체 위에서, 어떤 로직이 도메인 서비스에 있어야 하고 어떤 로직이 애플리케이션 서비스에 있어야 하는지 구분합니다.