본문 바로가기
개발 방법론

도메인 주도 설계 (DDD: Domain-Driven Design)

by 시니성 2023. 9. 6.
  1. 정의: DDD는 복잡한 문제를 해결하기 위해 도메인 전문가와 개발자가 긴밀하게 협력하여 현실 세계의 문제와 비즈니스 로직을 모델링하는 방식입니다.
  2. 기본 개념:
    • Entity: 고유 식별자를 가진 객체. 일반적으로 데이터베이스 테이블의 레코드와 일치.
    • Value Object: 불변의 값을 나타내는 객체. 식별자 없음.
    • Aggregate: 일련의 Entity와 Value Object의 그룹.
    • Repository: Aggregate를 저장하고 검색하는 메커니즘.
    • Domain Event: 도메인의 중요한 사건이 발생할 때 생성.
    • Service: 도메인 로직을 나타내지만 Entity나 Value Object에 속하지 않는 기능.
  3. 장점:
    • 모델 중심: 도메인 로직에 집중, 어플리케이션의 핵심을 명확하게 표현.
    • 품질 향상: 비즈니스 로직이 객체 지향적으로 구조화되어 코드의 재사용과 유지보수가 용이.
    • 비즈니스와 개발의 연계: 도메인 전문가와의 협력을 통해 더 나은 통신 및 이해 가능.
  4. 단점:
    • 시작 난이도: DDD를 시작하기 위한 초기 학습 곡선이 존재.
    • 과도한 설계: 간단한 어플리케이션에 DDD를 적용하면 과도한 설계로 이어질 수 있음.

전통적 개발 패러다임과의 차이점:

기존의 개발 패러다임은 데이터 중심이나 기능 중심으로 접근하기 쉽습니다. DDD는 비즈니스 로직과 도메인 중심으로 접근하여 개발과 비즈니스 간의 간극을 줄입니다.


아래는 "쇼핑몰"이라는 도메인에서 "주문"에 관한 로직을 다루는 상세한 시나리오를 가정하여 만든 DDD적용 코드와 기존 패러다임 코드입니다.

시나리오:
고객은 상품을 장바구니에 담습니다. 장바구니에 담긴 상품들을 주문합니다. 주문 시, 상품의 가격과 수량에 따른 전체 가격이 계산되며, 재고 확인 후 주문이 처리됩니다. 주문이 완료되면 재고가 감소하고, 주문 이력이 저장됩니다.


1. 기존 패러다임 코드 (절차적/객체지향 혼용) - Kotlin + Spring + JPA

@Entity
data class Product(
    val id: Long,
    val name: String,
    var stockQuantity: Int,
    val price: BigDecimal
)

@Entity
data class Order(
    val id: Long,
    val productId: Long,
    val quantity: Int,
    val totalPrice: BigDecimal
)

@Service
class OrderService(
    private val productRepository: ProductRepository,
    private val orderRepository: OrderRepository
) {
    fun placeOrder(productId: Long, quantity: Int) {
        val product = productRepository.findById(productId)
        if (product.stockQuantity < quantity) {
            throw Exception("Not enough stock")
        }

        val totalPrice = product.price.multiply(BigDecimal(quantity))
        val order = Order(null, productId, quantity, totalPrice)
        orderRepository.save(order)

        product.stockQuantity -= quantity
        productRepository.save(product)
    }
}

2. DDD 적용 코드

// === 도메인 ===

@Embeddable
data class Money(val amount: BigDecimal) {
    operator fun times(count: Int) = Money(amount.multiply(BigDecimal(count)))
}

@Entity
class Product(
    val id: Long,
    val name: String,
    private var stockQuantity: Int,
    val price: Money
) {
    fun decreaseStock(quantity: Int) {
        if (stockQuantity < quantity) throw DomainException("Not enough stock")
        this.stockQuantity -= quantity
    }

    fun calculatePrice(quantity: Int): Money = price * quantity
}

@Entity
class Order(
    val id: Long,
    val product: Product,
    val quantity: Int
) {
    val totalPrice: Money = product.calculatePrice(quantity)

    init {
        product.decreaseStock(quantity)
    }
}

// === 서비스 ===

@Service
class OrderService(
    private val productRepository: ProductRepository,
    private val orderRepository: OrderRepository
) {
    @Transactional
    fun placeOrder(productId: Long, quantity: Int): Order {
        val product = productRepository.findById(productId).orElseThrow { DomainException("Product not found") }
        val order = Order(null, product, quantity)
        return orderRepository.save(order)
    }
}

분석:

  • DDD 적용 코드에서는 Money라는 값 객체를 사용하여 금액을 표현하였습니다. 이는 도메인의 특정 측면(금액 계산)을 표현하는데 특화되었습니다.
  • Product 내에 재고를 감소시키는 로직(decreaseStock)과 가격을 계산하는 로직(calculatePrice)이 캡슐화되어 있습니다.
  • Order 생성시에 상품의 재고 감소와 가격 계산이 자동으로 이루어지도록 설계되었습니다. 이렇게 도메인 로직이 엔터티 안에 캡슐화되어 있으므로, 비즈니스 규칙이 흩어져 있지 않고 한 곳에서 관리됩니다.
  • 기존 패러다임에서는 서비스 계층에서 모든 로직이 처리되었으나, DDD에서는 가능한 많은 비즈니스 로직이 도메인 모델 내부에 위치합니다.

이 예제는 DDD의 여러 전략과 패턴 중 일부만을 보여주는 간단한 예시입니다. DDD를 실제 프로젝트에 적용하려면, 바운디드 컨텍스트, 애그리거트, 도메인 이벤트 등 다양한 전략과 패턴을 사용해야 합니다.


도메인 모델링: 비즈니스 요구사항과 문제 영역을 깊이 이해하고 모델링합니다. 이 과정에서 도메인 전문가와의 긴밀한 협업이 필요합니다.
바운디드 컨텍스트: 도메인을 여러 컨텍스트로 분리하여 각 컨텍스트 내에서 모델의 일관성을 유지합니다.
애그리거트: 데이터의 무결성을 유지하기 위해 관련된 객체들을 묶는 단위를 정의합니다.
리포지토리와 서비스: 애그리거트의 생명 주기를 관리하는 리포지토리와 도메인 로직을 수행하는 서비스를 정의합니다.
도메인 이벤트: 도메인의 상태 변화를 나타내는 이벤트를 정의하고 발행합니다.