
이전 포스팅에서 어떻게 폴더 구조로 나타나는지 보여줍니다.
핵심은 '애그리게이트를 외부로부터 격리'하는 것입니다.
가장 권장되는 헥사고날 아키텍처 기반의 구조입니다.
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 컨트롤러
애그리게이트 내부에서 불변식을 검증하고 이벤트를 발생하는 방식입니다.
// 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) // 결과적 일관성을 위한 이벤트 발행
}
}