You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 16 Current »

설계없이 시작가능한 AI에 의한 즉흥 개발방법도 지속적으로 유행하고 발전해 나가겠지만 상대적으로

느린 개발방법처럼 보이는 도메인 모델을 먼저 고민하고 시작하는 DDD(+이벤트 드리븐)  개발방법에서도 AI툴을 활용하는 방법이 없을까? 

고민하다가 시도된  변종실험으로 Qlerify를 간단하게 이용해보았으며 도메인모델을 통해 코드생성도 후속으로 시도해보았습니다.

(코틀린 코드와 액터모델로 구현시도됨)

 

시도된 프롬프트 내용은 이러한 정보 Style이 이용되었으며, 나머지는 AI응답에 의해 생성된 컨텐츠입니다.

 

이용된 AI  툴 :

도메인 모델및 이벤트 만들기

장바구니 기능을 만들고 싶음,단순하게 구매만 이용하는게 아닌

장바구니의 이벤트를 활용(담기/빼기..), 이벤트를 추적해 구매자의 최종 구매 심리적 요인과

구매예측을 만들고 싶음 기본 이벤트를 설계해.... 

 

 

용어설명

이 툴을 사용하기전 DDD에서 소개하는 개념을 간단하게 살펴보겠습니다. 

✅ CQRS 기본 개념

🔸 정의

CQRS는 명령(Command)조회(Query) 를 명확히 책임 분리(Responsibility Segregation) 하여 시스템을 설계하는 아키텍처 패턴입니다.

읽기와 쓰기 모델이 분리되어 각각 독립적으로 최적화될 수 있습니다.


🔄 CQRS 구성요소와 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와 명확히 분리됨


2. Aggregate Root & Read Model

Aggregate Root

  • 정의: 관련된 도메인 객체들의 일관성을 책임지는 루트 엔티티

  • 예시: OrderOrderItem들을 포함하지만, 외부에서는 Order를 통해서만 접근

  • 역할:

    • 모든 상태 변경은 Aggregate Root를 통해 수행

    • 도메인 규칙과 트랜잭션 경계를 보장

    • ID로 식별됨

  • 실제 코드 예시 (Kotlin):

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


3. Given - When - Then

  • 정의: 도메인 동작을 검증하기 위한 테스트 시나리오 패턴
    → BDD(Behavior-Driven Development) 스타일의 도메인 테스트에 사용

  • 형식:

    • Given: 초기 상태(이전 이벤트 또는 존재하는 Aggregate 상태)

    • When: 명령(command)을 실행할 때

    • Then: 예상되는 결과(이벤트 또는 상태 변화)

  • 예시 (Kotlin 테스트 코드 형태):

Given(주문이 생성되어 있고) When(상품을 추가하면) Then(상품이 주문에 포함되어야 한다)
given(OrderCreatedEvent) when(AddItemCommand) then(shouldEmit(ItemAddedEvent))

 

 

AI에 의해 완성된 도메인 이벤트/모델

 

 

 

Entities

 

 

User Story

  • 이 기반으로 규칙및 TC를 보강해 나갈수 있습니다. 

 

 

이벤트 드리븐 코드 생성 By GPT

이 도메인 모델을 참고해~ 이벤트를 처리하는 녀석들을 액터모델로 만들고
이벤트들을 퍼시던트를 이용해 저장시켜죠 

-도메인 모델이 이미지로 첨부되었습니다.

 

 

 

업로드하신 도메인 모델 이미지를 참고하고 기존 액터 구현 (HelloActor, BulkProcessor, HelloRouter) 코드 스타일을 기반으로 이벤트 기반 도메인 모델을 액터 기반으로 리팩토링하고 퍼시스턴스를 연동하는 예시를 제안드리겠습니다.


✅ 목표 요약

  • 도메인 이벤트를 처리하는 액터 정의

  • 이벤트들을 저장하기 위해 Pekko Persistence (Event Sourcing 방식) 연동

  • 기존 스타일에 맞춘 구현 (Router, Buffering 포함)


📌 전제 예시 (추정 도메인)

업로드하신 이미지를 분석한 결과, 다음과 같은 도메인 흐름이 있었습니다:

예: 고객 등록 이벤트 흐름

1. RegisterCustomerCommand
   → CustomerRegisteredEvent
   → CustomerState 업데이트

 

📦 1. 도메인 커맨드 및 이벤트 정의

// 명령
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 방식)

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. 라우터 및 벌크 처리 연동 (선택적 확장)

BulkProcessorHelloRouter처럼 다수 이벤트를 분산 처리할 경우:

val customerRouter = Routers.pool(5,
    Behaviors.supervise(CustomerPersistentActor.create("routered"))
        .onFailure(SupervisorStrategy.restart())
).withRoundRobinRouting()

 

💾 저장소 연동 설정 (HOCON 예시)

pekko.persistence.journal.plugin = "pekko.persistence.journal.inmem"
pekko.persistence.snapshot-store.plugin = "pekko.persistence.snapshot-store.local"



✅ 테스트 코드 힌트

testKit.spawn(CustomerPersistentActor.create("test")) 식으로 ActorTestKit을 활용한 검증도 가능하며, BulkProcessorTest.kt 스타일을 그대로 따를 수 있습니다.

필요 시 Snapshotting, DurableStateBehavior 기반 방식도 함께 설명드릴 수 있습니다.
다음으로 어떤 이벤트나 도메인을 우선 적용해볼까요?

 

최종 장바구니에 등록된 아이템을 단순하게 구매만 하기위한 기능도 필요함 
DurableStateBehavior  를 이용해 장단점도 설명

 

✅ DurableStateBehavior로 장바구니 액터 구현 (구매 확정용)

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 기반 액터 정의

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하려면:

.onCommand(Checkout::class.java) { state, cmd ->
    val purchased = state.items
    // publishToKafka(purchased) 등의 외부 IO 필요 시 외부 알림 처리 포함
    Effect().setState(state.clear())
        .thenReply(cmd.replyTo) { CheckoutSuccess(purchased) }
}

 

도메인에서 이벤트 소싱이 필요한 부분과 단순한 상태 저장이 필요한 부분을 잘 분리하면, 시스템 복잡도와 성능 모두 잡을 수 있습니다.
장바구니 외에 구매 주문 처리도 필요하신가요? (예: OrderPlacedOrderActor로 분리)

 

 

LLM등장 이전 만들어진 DDD와 같은 개발 방법론들이 , AI로 코드를 생성하는 시대에도 함께 발전해나갈지?

알수 없지만~ 이러한 연구를 하고 있는 활동의 링크를 공유해봅니다.

 

 

 

  • No labels