코드와 구조로 보는 DDD 전술적 패턴

이전 포스팅에서 어떻게 폴더 구조로 나타나는지 보여줍니다.
핵심은 '애그리게이트를 외부로부터 격리'하는 것입니다.

[실제 프로젝트 구조 예시: 주문(Order) 도메인]

가장 권장되는 헥사고날 아키텍처 기반의 구조입니다.

1. 폴더 구조

src/main/kotlin/com/example/shop/order/  <-- 바운디드 컨텍스트 경계
├── domain/                  <-- 외부 의존성 없음 
│   ├── model/               
│   │   ├── Order.kt         <-- 애그리게이트 루트
│   │   ├── OrderLine.kt     <-- 엔티티
│   │   ├── Address.kt       <-- 값 객체
│   │   └── OrderStatus.kt   <-- 합타입
│   ├── repository/          
│   │   └── OrderRepository.kt <-- 포트: 도메인이 요구하는 저장 기능
│   ├── service/             
│   │   └── DiscountPolicy.kt  <-- 여러 애그리게이트에 걸친 비즈니스 로직
│   └── events/              
│       └── OrderCreated.kt  <-- 도메인 이벤트
├── application/             <-- 유즈케이스 구현
│   ├── usecase/             
│   │   └── PlaceOrderService.kt <-- 트랜잭션 관리 및 도메인 로직 실행 흐름 제어
│   └── port/                
│       ├── input/           <-- 입력 포트
│       └── output/          <-- 출력 포트 
├── infrastructure/          <-- 외부 라이브러리 의존
│   ├── persistence/         
│   │   ├── JpaOrderRepository.kt <-- 리포지토리 실제 구현 
│   │   └── OrderEntity.kt    <-- DB 테이블 매핑용 객체
│   └── external/            
│       └── PgPaymentClient.kt <-- 결제 외부 API 연동 
└── ui/                      <-- 진입점
    └── controller/          
        └── OrderController.kt <-- REST API 컨트롤러

2. 실제 코드 예시

애그리게이트 내부에서 불변식을 검증하고 이벤트를 발생하는 방식입니다.

// Domain: Aggregate Root
class Order(
    val orderId: OrderId,
    val customerId: CustomerId,
    private val _orderLines: MutableList<OrderLine>,
    var status: OrderStatus = OrderStatus.PENDING // 합타입
) {
    // 항목은 최소 1개 이상이어야 함
    init {
        require(_orderLines.isNotEmpty()) { "주문 항목이 비어있을 수 없습니다." }
    }

    // 비즈니스 로직 
    fun completePayment() {
        if (this.status != OrderStatus.PENDING) throw IllegalStateException("결제 가능 상태가 아닙니다.")
        this.status = OrderStatus.PAID
        
        // 도메인 이벤트 생성
        recordEvent(OrderPaidEvent(orderId))
    }
}

// Application: Service
class PlaceOrderService(
    private val orderRepository: OrderRepository, // Interface
    private val eventPublisher: EventPublisher
) {
    @Transactional
    fun placeOrder(command: PlaceOrderCommand) {
        val order = Order(...) // 도메인 모델 생성
        orderRepository.save(order) // 저장
        eventPublisher.publish(order.events) // 결과적 일관성을 위한 이벤트 발행
    }
}
profile
에러가 나도 괜찮아 — 그건 내가 배우고 있다는 증거야.

0개의 댓글