Cute Light Pink Flying Butterfly 도메인 주도 설계, DDD하기 :: 놀면서 돈벌기
본문 바로가기
IT/Architecture

도메인 주도 설계, DDD하기

by esclife_ 2025. 12. 3.
반응형

 

DDD는 복잡한 비즈니스 로직을 가진 소프트웨어를 개발할 때,
비즈니스 도메인 전문가의 지식을 중심에 놓고 설계하는 접근 방식입니다.

 

 

1. DDD란 무엇인가?

📌 정의와 핵심 목적

도메인 주도 설계(Domain-Driven Design, DDD) 는 소프트웨어의 복잡성을 관리하고 해결하기 위해, 소프트웨어 코드를 비즈니스 도메인(업무 영역)과 밀접하게 일치시키는 것을 목표로 하는 개발 방법론입니다.

  • 도메인 (Domain): 소프트웨어가 해결하고자 하는 특정 업무 영역 (예: 금융, 의료, 쇼핑몰의 재고 관리 등).
  • 핵심 목적: 복잡한 도메인 지식을 코드로 정확하게 모델링하여, 비즈니스 요구사항 변경에 유연하게 대처하고 코드의 이해도를 높이는 것입니다.

🗣️ 유비쿼터스 언어 (Ubiquitous Language)

DDD의 가장 중요한 개념 중 하나입니다.

  • 정의: 개발팀, 마케팅팀, 그리고 도메인 전문가가 모두 동일한 의미로 사용하는 공통된 언어입니다.
  • 적용: 이 언어는 대화, 문서, 그리고 코드 자체에 그대로 반영되어야 합니다. 예를 들어, 도메인 전문가가 "주문(Order)"이라고 부르면, 코드에서도 Order 클래스나 order 변수명을 사용해야 합니다.

🗺️ 바운디드 컨텍스트 (Bounded Context)

  • 정의: 유비쿼터스 언어가 의미를 갖는 명확한 경계입니다.
  • 필요성: 같은 단어라도 시스템의 영역에 따라 의미가 다를 수 있습니다.
    • 예시: '고객(Customer)'이라는 단어는 '판매(Sales)' 컨텍스트에서는 "잠재 구매자"를 의미하지만, '배송(Shipping)' 컨텍스트에서는 "배송 주소와 연락처를 가진 사람"을 의미합니다.
  • DDD는 복잡한 시스템을 이처럼 독립적인 바운디드 컨텍스트로 나누어 관리합니다.

2. DDD의 핵심 구성 요소

A. 엔티티 (Entity)

  • 특징: 고유한 식별자(ID)를 가지며, 시간의 흐름에 따라 상태(속성)가 변해도 동일성(Identity)이 유지되는 객체입니다.
  • 역할: 비즈니스 로직과 행위(Behavior)를 담고 있습니다.
  • 예시: Order (주문 번호가 ID), Customer (고객 ID).

B. 값 객체 (Value Object)

  • 특징: 고유한 식별자가 없으며, 자신의 속성(값)으로 동일성을 판단합니다. 불변성(Immutable)을 가져야 합니다.
  • 역할: 특정 값이나 개념을 표현하며, 여러 속성을 묶어 의미를 명확하게 합니다.
  • 예시: Address (우편번호, 시/도, 도로명 주소 값이 모두 같으면 동일한 Address), Money (금액, 통화).

C. 애그리게이트 (Aggregate)

  • 특징: 엔티티와 값 객체들의 묶음이자 트랜잭션의 경계입니다.
  • 역할: 애그리게이트 내부 객체들 간의 일관성을 보장하며, 외부에서는 루트 엔티티(Aggregate Root)를 통해서만 접근해야 합니다.
  • 예시: Order 애그리게이트는 Order 엔티티(루트), 여러 개의 LineItem 값 객체, ShippingAddress 값 객체 등으로 구성될 수 있습니다.

D. 도메인 서비스 (Domain Service)

  • 특징: 특정 엔티티나 값 객체에 속하기 어려운, 여러 애그리게이트에 걸쳐있는 비즈니스 로직이나 도메인 행위를 처리합니다.
  • 예시: 두 계좌(Account 애그리게이트 1, 2) 간의 '금액 이체(Transfer)' 로직.

3. DDD 구현 예시 및 Python 코드

쇼핑몰에서 '주문(Order)' 도메인을 DDD 방식으로 모델링하는 코드를 살펴보겠습니다.

📋 도메인 모델 설계

  • 애그리게이트 루트: Order
  • 엔티티: Order, Product (외부 시스템 ID 필요)
  • 값 객체: Money, LineItem (수량과 제품 정보를 묶음)

🐍 Python 코드 구현 (예시)

1. 값 객체 (Value Object): Money

from dataclasses import dataclass

@dataclass(frozen=True) # frozen=True로 불변성 확보
class Money:
    """값 객체: 금액과 통화 단위를 표현"""
    amount: float
    currency: str

    def add(self, other: 'Money') -> 'Money':
        if self.currency != other.currency:
            raise ValueError("통화 단위가 다릅니다.")
        return Money(self.amount + other.amount, self.currency)

# 사용 예: price = Money(15.99, "USD")

2. 엔티티 (Entity): Product (일부 속성 생략)

class Product:
    """엔티티: 고유한 ID(SKU)를 가지는 상품"""
    def __init__(self, sku: str, name: str, price: Money):
        if not sku:
            raise ValueError("SKU는 필수입니다.")
        self.sku = sku  # 고유 식별자
        self.name = name
        self.price = price

3. 애그리게이트 루트 (Aggregate Root): Order

from typing import List

@dataclass(frozen=True)
class LineItem:
    """값 객체: 주문 항목 (상품 정보와 수량을 묶음)"""
    product_sku: str
    quantity: int
    price: Money

class Order:
    """애그리게이트 루트: 주문 전체의 트랜잭션 경계"""
    def __init__(self, order_id: str, customer_id: str, items: List[LineItem] = None):
        if not order_id:
            raise ValueError("주문 ID는 필수입니다.")
        
        self.order_id = order_id     # 엔티티의 고유 식별자
        self.customer_id = customer_id
        self._items = items if items is not None else []
        self._status = "PENDING"

    # --- 도메인 행위 (Behavior) ---
    def add_item(self, item: LineItem):
        """주문 항목 추가 행위 (애그리게이트 내부 일관성 유지)"""
        if self._status != "PENDING":
            raise PermissionError("이미 처리된 주문에는 항목을 추가할 수 없습니다.")
        self._items.append(item)

    def calculate_total(self) -> Money:
        """총 주문 금액 계산"""
        total = Money(0.0, "USD") # 초기 금액
        for item in self._items:
            # item.price는 LineItem의 값 객체 Money입니다.
            item_total = Money(item.price.amount * item.quantity, item.price.currency)
            total = total.add(item_total)
        return total

    def confirm_order(self):
        """주문 확정 행위"""
        if not self._items:
            raise ValueError("주문 항목이 없습니다.")
        if self._status == "PENDING":
            self._status = "CONFIRMED"
        else:
            raise ValueError(f"현재 상태({self._status})에서는 주문을 확정할 수 없습니다.")
            
    # Getter: 외부에서는 내부 리스트를 직접 수정할 수 없도록 사본을 반환
    @property
    def items(self) -> List[LineItem]:
        return list(self._items) 

🎯 DDD의 효과

이 코드는 단순한 데이터 구조(DTO)가 아닌, 도메인의 행위(add_item, confirm_order, calculate_total)를 클래스 내부에 캡슐화하고 있습니다.

  • 일관성 보장: 주문 확정(confirm_order) 전에 항목이 있는지(if not self._items) 확인하거나, 이미 확정된 주문에 항목 추가를 막는 등의 비즈니스 규칙이 Order 클래스 안에 명확하게 정의되어, 코드가 비즈니스 요구사항을 정확히 반영합니다.

 


참고 자료

 

https://blog.kakaocloud.com/193

 

<지식 사전> 도메인 주도 설계(Domain Driven Design)란? - 비즈니스 언어로 소프트웨어 만들기

🧑‍💻 요약 도메인 주도 설계(DDD)가 왜 필요한지, 기존 개발 방식의 문제(소통 단절)를 어떻게 해결하는지 설명합니다. 공통 언어·바운디드 컨텍스트·엔티티/값 객체·애그리게이트 등 DDD 핵

blog.kakaocloud.com

https://tech.kakaopay.com/post/backend-domain-driven-design/

 

카카오페이 여신코어 DDD(Domain Driven Design, 도메인 주도 설계)로 구축하기 | 카카오페이 기술 블로

카카오페이의 여신업무 내재화 프로젝트를 DDD를 적용하여 구축한 내용을 공유하고자 합니다.

tech.kakaopay.com

 

반응형