Page History
...
느린 개발방법처럼 보이는 도메인 모델을 먼저 고민하고 시작하는 DDD(+이벤트 드리븐) 개발방법에서도 AI툴을 활용하는 활용해 가속화하는 방법이 없을까?
고민하다가 시도된 변종실험으로 Qlerify를 간단하게 이용해보았으며 도메인모델을 통해 코드생성도 후속으로 시도해보았습니다.
(코틀린 코드와 액터모델로 구현시도됨)
| Info |
|---|
시도된 프롬프트 내용은 이러한 정보 Style이 이용되었으며, 나머지는 AI응답에 의해 생성된 컨텐츠입니다. |
이용된 AI 툴 :
- 도메인 모델 생성 :https://app.qlerify.com/
- 코드생성 : GPT 프로젝트
도메인 모델및 이벤트 만들기
| Info |
|---|
장바구니 기능을 만들고 싶은 싶음,단순하게 장바기느를 구매만 이용하는게 아닌 장바구니의 이벤트를 활용(담기/빼기..), 이벤트를 추적해 구매자의 최종 구매 심리적 요인과 구매예측을 만들고 싶음 기본 이벤트를 설계해.... |
용어설명
이 툴을 사용하기전 DDD에서 소개하는 개념을 간단하게 살펴보겠습니다.
...
| 개념 | 설명 | DDD 요소와 연결 |
|---|---|---|
| Command | 데이터를 변경하는 요청 (예: 주문 생성) | Command, Aggregate Root, When |
| Command Handler | 명령을 받아 도메인 로직 실행 (비즈니스 규칙 검증 포함) | 도메인 서비스 or 애그리거트 메서드 |
| Aggregate Root | 실제 비즈니스 로직이 작동하는 단위. 상태 변경을 캡슐화 | Aggregate Root |
| Event | Command 처리 후 발생하는 결과 (이벤트 소싱일 경우 저장됨) | Then, 이벤트 핸들러 |
| Query | 데이터를 조회하는 요청 (상태 변경 없음) | Read Model, Projection, DTO |
| Read Model | 조회에 최적화된 모델 (DB View, Cache, Elastic 등) | Read Model, Given |
1. Command (명령)
정의: 시스템에 "무언가를 하라"고 요청하는 객체.
예시:
CreateOrderCommand,ChangeUserEmailCommand특징:
상태를 변경하는 의도(Intent) 를 나타냄
보통 쓰기 작업(write) 에 해당하며, 반환값은 없거나 간단한 결과 (ex: 성공/실패, ID 등)
컨트롤러나 API에서 입력값을 받아 생성됨
DDD 문맥에서 역할:
Aggregate에게 전달되어 비즈니스 규칙에 따라 실행됨
CQRS 구조에서는 Query와 명확히 분리됨
...
정의: 관련된 도메인 객체들의 일관성을 책임지는 루트 엔티티
예시:
Order는OrderItem들을 포함하지만, 외부에서는Order를 통해서만 접근역할:
모든 상태 변경은 Aggregate Root를 통해 수행
도메인 규칙과 트랜잭션 경계를 보장
ID로 식별됨
실제 코드 예시 (Kotlin):
| Code Block | ||
|---|---|---|
| ||
class Order(private val id: UUID, private val items: List<OrderItem>) { fun addItem(item: OrderItem) { /* 비즈니스 규칙 검증 후 추가 */ } } |
⮕ Read Model (읽기 전용 모델)
정의: 조회를 빠르게 하기 위해 최적화된 별도의 데이터 구조
특징:
Command 모델과 분리됨 (특히 CQRS에서 중요)
보통 DB View, Projection, DTO 등으로 구성
일관성보다는 응답 속도를 중시함
예시:
OrderSummaryView,UserDashboardDto
...
정의: 도메인 동작을 검증하기 위한 테스트 시나리오 패턴
→ BDD(Behavior-Driven Development) 스타일의 도메인 테스트에 사용형식:
Given: 초기 상태(이전 이벤트 또는 존재하는 Aggregate 상태)
When: 명령(command)을 실행할 때
Then: 예상되는 결과(이벤트 또는 상태 변화)
예시 (Kotlin 테스트 코드 형태):
| Code Block | ||
|---|---|---|
| ||
Given(주문이 생성되어 있고) When(상품을 추가하면) Then(상품이 주문에 포함되어야 한다) |
| Code Block | ||
|---|---|---|
| ||
given(OrderCreatedEvent) when(AddItemCommand) then(shouldEmit(ItemAddedEvent)) |
AI에 의해 완성된 도메인 이벤트/모델
Entities
User Story
- 이 기반으로 규칙및 TC를 보강해 나갈수 있습니다.
이벤트 드리븐 코드 생성 By GPT
| Info |
|---|
이 도메인 모델을 참고해~ 이벤트를 처리하는 녀석들을 액터모델로 만들고 -도메인 모델이 이미지로 첨부되었습니다. |
- 액터모델을 이용한 언어및 유용한 패턴을 참조
업로드하신 도메인 모델 이미지를 참고하고 기존 액터 구현 (HelloActor, BulkProcessor, HelloRouter) 코드 스타일을 기반으로 이벤트 기반 도메인 모델을 액터 기반으로 리팩토링하고 퍼시스턴스를 연동하는 예시를 제안드리겠습니다.
...
예: 고객 등록 이벤트 흐름
| Code Block | ||
|---|---|---|
| ||
1. RegisterCustomerCommand
→ CustomerRegisteredEvent
→ CustomerState 업데이트
|
📦 1. 도메인 커맨드 및 이벤트 정의
| Code Block | ||
|---|---|---|
| ||
// 명령
sealed interface CustomerCommand
data class RegisterCustomer(val name: String, val replyTo: ActorRef<CustomerResponse>) : CustomerCommand
// 이벤트
sealed interface CustomerEvent
data class CustomerRegistered(val customerId: String, val name: String) : CustomerEvent
// 상태
data class CustomerState(val customers: Map<String, String> = emptyMap()) {
fun applyEvent(event: CustomerRegistered): CustomerState {
return copy(customers = customers + (event.customerId to event.name))
}
}
// 응답
sealed interface CustomerResponse
data class RegisterSuccess(val customerId: String) : CustomerResponse |
🎭 2. 상태 저장형 액터 (Event Sourced 방식)
| Code Block | ||
|---|---|---|
| ||
class CustomerPersistentActor private constructor(
context: ActorContext<CustomerCommand>,
persistenceId: PersistenceId,
) : EventSourcedBehavior<CustomerCommand, CustomerEvent, CustomerState>(
persistenceId,
emptyState = CustomerState(),
commandHandler = commandHandler(),
eventHandler = eventHandler()
) {
companion object {
fun create(entityId: String): Behavior<CustomerCommand> {
return Behaviors.setup { context ->
CustomerPersistentActor(
context,
PersistenceId.ofUniqueId("Customer-$entityId")
)
}
}
private fun commandHandler(): CommandHandler<CustomerCommand, CustomerEvent, CustomerState> {
return newCommandHandlerBuilder()
.forAnyState()
.onCommand(RegisterCustomer::class.java) { state, cmd ->
val event = CustomerRegistered(UUID.randomUUID().toString(), cmd.name)
Effect().persist(event)
.thenReply(cmd.replyTo) { newState -> RegisterSuccess(event.customerId) }
}
.build()
}
private fun eventHandler(): EventHandler<CustomerState, CustomerEvent> {
return newEventHandlerBuilder()
.forAnyState()
.onEvent(CustomerRegistered::class.java) { state, event ->
state.applyEvent(event)
}
.build()
}
}
} |
🚏 3. 라우터 및 벌크 처리 연동 (선택적 확장)
BulkProcessor나 HelloRouter처럼 다수 이벤트를 분산 처리할 경우:
| Code Block | ||
|---|---|---|
| ||
val customerRouter = Routers.pool(5,
Behaviors.supervise(CustomerPersistentActor.create("routered"))
.onFailure(SupervisorStrategy.restart())
).withRoundRobinRouting() |
💾 저장소 연동 설정 (HOCON 예시)
| Code Block | ||
|---|---|---|
| ||
pekko.persistence.journal.plugin = "pekko.persistence.journal.inmem"
pekko.persistence.snapshot-store.plugin = "pekko.persistence.snapshot-store.local" |
...
필요 시 Snapshotting, DurableStateBehavior 기반 방식도 함께 설명드릴 수 있습니다.
다음으로 어떤 이벤트나 도메인을 우선 적용해볼까요?
| Info |
|---|
최종 장바구니에 등록된 아이템을 단순하게 구매만 하기위한 기능도 필요함 |
✅ DurableStateBehavior로 장바구니 액터 구현 (구매 확정용)
| Code Block | ||
|---|---|---|
| ||
sealed interface CartCommand
data class AddItem(val itemId: String, val replyTo: ActorRef<CartResponse>) : CartCommand
data class Checkout(val replyTo: ActorRef<CartResponse>) : CartCommand
sealed interface CartResponse
data class CartUpdated(val items: List<String>) : CartResponse
data class CheckoutSuccess(val purchased: List<String>) : CartResponse
data class CartState(val items: List<String> = emptyList()) {
fun add(itemId: String) = copy(items = items + itemId)
fun clear() = copy(items = emptyList())
}
|
🎭 DurableState 기반 액터 정의
| Code Block | ||
|---|---|---|
| ||
class CartActor private constructor(
context: ActorContext<CartCommand>,
persistenceId: PersistenceId
) : DurableStateBehavior<CartCommand, CartState>(persistenceId) {
override fun emptyState(): CartState = CartState()
companion object {
fun create(userId: String): Behavior<CartCommand> {
return Behaviors.setup { context ->
CartActor(context, PersistenceId.ofUniqueId("cart-$userId"))
}
}
}
override fun commandHandler(): CommandHandler<CartCommand, CartState> {
return newCommandHandlerBuilder()
.forAnyState()
.onCommand(AddItem::class.java) { state, cmd ->
val updated = state.add(cmd.itemId)
Effect().setState(updated)
.thenReply(cmd.replyTo) { CartUpdated(updated.items) }
}
.onCommand(Checkout::class.java) { state, cmd ->
val purchased = state.items
Effect().setState(state.clear())
.thenReply(cmd.replyTo) { CheckoutSuccess(purchased) }
}
.build()
}
}
|
⚖️ DurableStateBehavior vs EventSourcedBehavior
| 항목 | DurableStateBehavior | EventSourcedBehavior |
|---|---|---|
| 저장 전략 | 최종 상태만 저장 (snapshot-like) | 모든 이벤트를 append-only log로 저장 |
| 조회 성능 | 빠름 (단일 상태 조회) | 느림 (리플레이 필요) |
| 디스크 사용량 | 적음 (단일 상태만 저장) | 많음 (전체 이벤트 저장) |
| 롤백/감사 추적 | 불가능 or 제한적 | 완전한 시간 순 추적 가능 |
| 주 용도 | CRUD 스타일, 단순 상태 저장 (ex. 장바구니, 설정 등) | 이벤트 소싱 기반 복원 필요 시스템 |
| 복원 속도 | 매우 빠름 | 느릴 수 있음 (스냅샷 없을 시 전체 이벤트 재생) |
| 복잡도 | 단순함 | 상대적으로 복잡함 (이벤트 설계 필요)
|
✅ 사용 예 요약
| Use Case | 권장 방식 |
|---|---|
| 장바구니 구매 확정 | ✅ DurableStateBehavior |
| 주문 처리 로그 추적 | ❗ EventSourcedBehavior |
| 사용자 프로필 설정 저장 | ✅ DurableStateBehavior |
| 이력 기반 복원 가능한 상태 | ❗ EventSourcedBehavior |
🔄 확장 아이디어
DurableStateBehavior를 기반으로 하되, CheckoutSuccess를 외부 Kafka/DB 등에 이벤트로 publish하려면:
| Code Block | ||
|---|---|---|
| ||
.onCommand(Checkout::class.java) { state, cmd ->
val purchased = state.items
// publishToKafka(purchased) 등의 외부 IO 필요 시 외부 알림 처리 포함
Effect().setState(state.clear())
.thenReply(cmd.replyTo) { CheckoutSuccess(purchased) }
}
|
도메인에서 이벤트 소싱이 필요한 부분과 단순한 상태 저장이 필요한 부분을 잘 분리하면, 시스템 복잡도와 성능 모두 잡을 수 있습니다.
장바구니 외에 구매 주문 처리도 필요하신가요? (예: OrderPlaced → OrderActor로 분리)
| Note |
|---|
LLM등장 이전 만들어진 DDD와 같은 개발 방법론들이 , AI로 코드를 생성하는 시대에도 함께 발전해나갈지? 알수 없지만~ 이러한 연구를 하고 있는 활동의 링크를 공유하면서 마무리해봅니다공유해봅니다. |




