최범균 님의 "도메인 주도 개발 시작하기: DDD 핵심 개념 정리부터 구현까지" 책을 정리한 포스팅 입니다.
1. 여러 애그리거트가 필요한 기능
- 도메인 영역의 코드를 작성하다 보면, 한 애그리거트로 기능을 구현할 수 없을 때가 있음
- ✅ 도메인 / 애플리케이션 서비스에서 처리하기
- ❌ 여러 애그리거트의 데이터/규칙을 한 번에 써야하는 유스케이스는, 한 애그리거트에 집어넣지 말기
예) Order - 결제 금액 계산
더보기
주문을 확정하려면 주문 애그리거트는 외부 애그리거트 정보를 읽어와서 계산하고, 결과 스냅샷만 자기 내부에 기록함
애그리거트 | 주문 시 필요한 정보(읽기) | 소유/책임 | 비고 |
Product | 상품 가격, 판매 가능 여부, 카테고리 | 상품 카탈로그/정책 |
가격은 주문 시점 스냅샷으로 Order에 저장
|
Coupon/Promotion | 쿠폰 유효성, 할인 방식(정액/정율), 중복/제약(카테고리 등) |
할인 도메인 |
적용 결과(할인내역)를 스냅샷으로 기록
|
User/Member | 회원 등급/상태, 등급별 추가 할인 여부 | 회원 도메인 |
개인화 정책 판단용 읽기
|
Shipping/Policy | 배송비 규칙(무료 조건, 지역/금액별) | 배송/정책 도메인 |
계산된 배송비를 스냅샷으로 기록
|
// 포트(외부 호출 인터페이스) - 도메인이 아는 건 '계약'뿐
public interface ProductPricingPort {
Map<ProductId, Money> getPrices(Set<ProductId> ids);
}
public interface MemberTierPort {
MemberTier getTier(UserId userId);
}
// 순수 도메인 서비스(계산 전용, I/O 없음)
public final class PricingCalculator {
public Pricing calc(List<OrderLineReq> lines,
Map<ProductId, Money> prices,
MemberTier tier,
ShippingPolicy policy) {
// 할인/배송비/총액 계산…
}
}
// 애그리거트(네트워크 X, 자기 상태만)
public class Order {
public static Order place(UserId userId, List<OrderLine> lines, Pricing snapshot) {
// 불변식 검증 + 스냅샷 적용
}
}
// 애플리케이션 서비스(오케스트레이션 담당)
@Transactional
public class OrderAppService {
private final ProductPricingPort pricingPort; // 인프라 어댑터가 구현
private final MemberTierPort memberPort;
private final PricingCalculator calculator;
private final OrderRepository orders;
public OrderId place(PlaceOrderCmd cmd) {
var prices = pricingPort.getPrices(cmd.productIds()); // 네트워크 호출(포트)
var tier = memberPort.getTier(cmd.userId()); // 네트워크 호출(포트)
var pricing = calculator.calc(cmd.lines(), prices, tier, ShippingPolicy.current());
var order = Order.place(cmd.userId(), toLines(cmd), pricing.snapshot());
orders.save(order);
// outbox 이벤트 발행 등
return order.getId();
}
}
2. 도메인 서비스
- 도메인 영역에 위치한 도메인 로직을 표현할 때 사용함
도메인 서비스
- 여러 애그리거트가 필요한 계산 로직 담당
- 도메인 개념을 명시적으로 드러내기 (상태 없이 로직만 구현)
예시) Order - 결제 금액 계산
더보기
// 도메인 포트: 외부 값을 '읽기'로만 받기 위한 계약
public interface PricingPolicyPort {
BigDecimal getDiscountRate(MemberTier tier); // 읽기 전용
}
// 도메인 서비스: 순수 계산(부작용 없음)
public final class PricingCalculator {
private final PricingPolicyPort policy; // 포트에만 의존
public Money calc(List<OrderLine> lines, MemberTier tier, Money shippingFee) {
var subtotal = lines.stream().map(OrderLine::subtotal).reduce(Money.zero(), Money::plus);
var rate = policy.getDiscountRate(tier); // 읽기
var discount = subtotal.percent(rate);
return subtotal.minus(discount).plus(shippingFee);
}
}
애플리케이션 서비스
- 유스케이스 오케스트레이션 담당
- 구현하기 위해 외부 시스템을 연동이 필요한 도메인 로직에 사용됨
예시) Order - 결제 금액 계산
더보기
@Transactional
public class OrderAppService {
private final ProductReader productReader; // 외부/다른 애그리거트 조회
private final MemberReader memberReader;
private final ShippingPolicyProvider policyProvider;
private final PricingCalculator calculator; // 도메인 서비스
private final OrderRepository orders;
public OrderId place(PlaceOrderCmd cmd) {
var prices = productReader.loadPrices(cmd.productIds()); // 외부 조회(I/O)
var tier = memberReader.loadTier(cmd.userId()); // 외부 조회(I/O)
var ship = policyProvider.current(); // 외부 조회(I/O)
var lines = toOrderLines(cmd.lines(), prices); // 도메인 입력 준비
var total = calculator.calc(lines, tier, ship.fee()); // 순수 계산
var order = Order.place(cmd.userId(), lines, total.snapshot());
orders.save(order);
// 결제 승인/재고 예약 등 '부작용'은 여기서 커맨드/사가로 처리
return order.getId();
}
}
도메인 서비스의 패키지 위치
도메인 서비스의 인터페이스와 클래스
'Code' 카테고리의 다른 글
[도메인 주도 개발 시작하기: DDD 핵심 개념 정리부터 구현까지] 9. 도메인 모델과 바운디드 컨텍스트 (0) | 2025.06.20 |
---|---|
[도메인 주도 개발 시작하기: DDD 핵심 개념 정리부터 구현까지] 8. 애그리거트 트랜잭션 관리 (0) | 2025.06.19 |
[도메인 주도 개발 시작하기: DDD 핵심 개념 정리부터 구현까지] 4. 애그리거트 (0) | 2025.06.19 |
[도메인 주도 개발 시작하기: DDD 핵심 개념 정리부터 구현까지] 3. 애그리거트 (0) | 2025.06.18 |
[도메인 주도 개발 시작하기: DDD 핵심 개념 정리부터 구현까지] 2. 아키텍처 개요 (0) | 2025.06.18 |