Self-Development/Study

[DDD] 애그리거트(Aggregate) 디자인하기

JeongKyun 2024. 6. 7.
반응형

# 서론

일단 제목에서 나온 키워드의 사전적 개념부터 간단히 정리하고 시작한다.

그리고 애그리거트를 왜, 어떻게 그리고 이를 어떻게 활용하는지에 대한 내용을 소개해보려한다.

 

애그리거트(Aggregate)

데이터 변경의 단위로 다루는 연관 객체의 불변 집합체를 말한다.

 

(사전적 개념만 정리했을땐 간단해보이지만, 애그리거트 개념은 굉장히 많은 내용을 내포하고있어 더 깊이 알아볼 필요가있다.)

 

# 본론

왜 애그리거트 (Aggregate) 경계 설정이 중요할까?

가장 대중적인 주문(Order) 도메인으로 설명을 해보려한다.

필자는 주문 시스템을 만들기 위한 엔터티들을 아래와 같이 설정했다.

 

data class Address(
    val street: String,
    val city: String,
    val state: String,
    val zipCode: String
)

class OrderLineItem(
    val id: UUID,
    val productId: UUID,
    val quantity: Int,
    val price: Double
) {
	//...
}

class Order(
    val id: UUID = UUID.randomUUID(),
    val customerId: UUID,
    var shippingAddress: Address,
    private val lineItems: MutableList<LineItem> = mutableListOf()
) {
	//...
}

 

주문 시스템을 만들기 위한 최소한의 연관 객체들을 정의해보았다.

* 주문

* 주문항목

* 주소

 

이제 위 연관 객체들로 고객이 주문을 하는 유즈케이스를 구현하면 다음과 같을것이다.

PlaceOrderUseCase

class PlaceOrderUseCase(
	private val orderRepository: OrderRepository
) {

    fun execute(order: Order) {
        validateShippingAddress(order.shippingAddress)
        validateTotalAmount(order)

        orderRepository.save(order)
    }

    private fun validateShippingAddress(shippingAddress: Address?) {
        requireNotNull(shippingAddress) { "Shipping address is required" }
    }

    private fun validateTotalAmount(order: Order) {
        val totalAmount = order.getLineItems().sumOf { it.price * it.quantity }
        require(totalAmount > 0) { "Total amount must be greater than zero" }
    }
    
    //...
}

interface OrderRepository {
    fun save(order: Order)
}

 

위 코드에선 주문을 할 때 주소의 값이 비어있는것과, 구매 항목들의 총합이 0이 반드시 넘어야 한다는 지켜져야 할 비지니스 규칙을 검증하고있다. 주문이 성공한다면, 시스템에선 해당 주문을 영속(persist)하는것이니 마지막 라인에선 save를 하고있다.

 

(* 물론 비지니스 규칙은 곧 제품 정책이기에 주문 시스템을 만들 때 회사마다 다른 규칙일 수 있다)

 

위 구조를 객체 간 관계를 나타내보면 다음과 같다.

 

도식화를 보면 Service에 속한 PlaceOrderUseCase가 Order, Address, OrderLineItem 등 모든 객체를 참조하고있다. 

그럼 해당 UseCase를 구현할 때마다 해당 규칙들은 파편화가 되고 응집된 모델이 될 수 없다는 이야기와 같다.

 

아마도 주문을 할 때 위 비지니스 규칙은 반드시 지켜져야 할 규칙일것이다. 또한, 주문 정보를 수정하고 불러오고 하는 등 주문 행위와 관련된 모든 유즈케이스에서 고려해야할 검증식이된다.

 

실제 주문 시스템을 만든다면 아마 더 많은 검증식들이 필요로 할것이고 훨씬 더 복잡한 객체(엔터티, 값 객체)들의 연관 관계가 형성될것이고, 이를 일관성있게 시스테믹하게 관리하는것은 힘들어진다.

 

DDD 저자인 에릭 에반스는 다음과 같은 말을 했다.

모델 내에서 복잡한 연관 관계를 맺는 객체를 대상으로 변경의 일관성을 보장하기란 쉽지 않다.
그 까닭은 단지 개별 객체만이 아닌 서로 밀접한 관계에 있는 객체 집합에도 불변식이 적용돼야 하기 때문이다.

이 말을 지금 예제 도메인으로 투영해보면, 주문을 할 때 검증해야할 비지니스 규칙에는 주문(Order) 하나만으로 검증할 수 있는게 아닌 주문항목, 주소 등 밀접한 관계에 있는 객체 집합에도 불변식이 적용되어야 한다는 뜻이다.

 

이해를 위해 에릭 에반스의 도메인 주도 설계 책에서 말한 내용을 하나 더 인용해보면,

모델 내의 참조에 대한 캡슐화를 추상화할 필요가 있다.
애그리거트는 우리가 데이터 변경의 단위로 다루는 연관 객체의 묶음을 말한다.
각 애그리거트에는 루트(root)와 경계(boundary)가 있다.
경계는 애그리거트에 무엇이 포함되고 포함되지 않는지를 정의한다.
루트는 단 하나만 존재하며, 애그리거트에 포함된 특정 엔터티를 가리킨다.
경계 안의 객체는 서로 참조할 수 있지만, 경계 바깥의 객체는 해당 애그리거트의 구성요소 가운데 루트만 참조할 수 있다.

루트 이외의 엔터티는 지역 식별성(local identity)을 지니며, 지역 식별성은 애그리거트 내에서만 구분되면 된다.
이는 해당 애그리거트의 경계 밖에 위치한 객체는 루트 엔터티의 컨텍스트 말고는 애그리거트의 내부를 볼 수 없기 때문이다.
(도메인 주도 개발 - Eric evans)

즉, 위 시스템에선 주문 시스템에 대한 모든 컨텍스트를 가지고 핸들링 할 수 있고 이를 통해 전역 식별자로 제어할 수 있는 주문(Order)가 루트 애그리거트가 될것이다.

 

이제 루트 애그리거트를 설정하였으니, 비지니스 규칙인 도메인의 불변식들을 애그리거트에 응집시켜보는 리팩터링 시간을 가져보자.

 

어떻게 애그리거트 (Aggregate) 구현할까?

Refactor To Be

class Order(
    val id: UUID = UUID.randomUUID(),
    val customerId: UUID,
    var shippingAddress: Address?,
    private val lineItems: MutableList<LineItem> = mutableListOf()
) {

    fun placeOrder() {
        validateShippingAddress()
        validateTotalAmount()
        //...
    }
    
    fun getTotalAmount(): Double {
        return this.lineItems.sumOf { it.price * it.quantity }
    }
    
    private fun validateShippingAddress() {
        requireNotNull(this.shippingAddress) { "Shipping address is required" }
    }

    private fun validateTotalAmount() {
        val totalAmount = getTotalAmount()
        require(totalAmount > 0) { "Total amount must be greater than zero" }
    }

    //...
}

class PlaceOrderUseCase(
	private val orderRepository: OrderRepository
) {

    fun execute(order: Order) {
        order.placeOrder()
        orderRepository.save(order)
    }
}

interface OrderRepository {
    fun save(order: Order)
}

 

 

 

위 구조는 이제 주문(Order)라는 루트 애그리거트에서 placeOrder를 수행하여 도메인 불변식을 검증하고있다.

훨씬 주문이라는 도메인 모델이 풍부해지고 도메인의 불변식들이 루트에 응집되어 지켜져야 할 객체 간 일관성들이 잘 지켜지고 있는것같지않은가?

 

 

또한 To Be의 의존 관계를 도식화 해보면 훨씬 단순해진것도 알 수 있다.

참고로 이번 예제 코드에선 다루진않았지만 각자의 값 객체, 도메인 엔터티 등들의 각자의 불변식도 내부에 응집된 형태로 구현했을것이다. 즉, 루트 애그리거트의 불변식은 도메인 모델이 수행하고자 하는 행위들에 대한 많은 엔터티들의 관계에서 지켜져야할 일관성을 보장하기 위한 식일뿐이며 관계가 형성되어있는 값 객체, 엔터티 등 모두 개별로 지켜져야 할 불변식들이 존재할 것이다.

 

 

# 마무리

이번 글에선 애그리거트의 개념을 이해하고, 경계를 설정하여 풍부한 도메인 모델을 만들어나가는 과정을 짤막하게 다뤄보았다. 위에서도 말했듯이 현업에서의 도메인 모델은 더욱 더 복잡한 관계들로 형성되어있다. 

 

복잡한 관계를 좀 더 간단하게 일관성과 불변식들을 지켜나갈 수 있는 형태로 만들어나가기 위한 기법을 토대로 앞으로 만들어야하는 시스템을 디자인 해나가는 과정에서 도움이 조금이나마 되었으면 좋겠다.

댓글

💲 많이 본 글