Skip to content
ioob.dev
Go back

DDD 1편 — 도메인 중심 설계가 필요한 이유

· 12분 읽기
DDD 시리즈 (1/7)
  1. DDD 1편 — 도메인 중심 설계가 필요한 이유
  2. DDD 2편 — 유비쿼터스 언어와 Bounded Context
  3. DDD 3편 — Context Mapping
  4. DDD 4편 — Entity와 Value Object
  5. DDD 5편 — Aggregate와 Repository
  6. DDD 6편 — Domain Service와 Application Service
  7. DDD 7편 — Domain Event와 Anti-Corruption Layer
Table of contents

Table of contents

기술이 문제가 아니었다

프로젝트가 실패하는 이유를 돌아보면, 기술 선택이 잘못돼서인 경우는 드물다. Spring Boot를 써야 했는데 Express를 썼다거나, MySQL 대신 PostgreSQL을 골랐다거나 — 그런 이유로 프로젝트가 무너지는 일은 거의 없다.

진짜 문제는 다른 곳에서 터진다. 기획자가 주문이라고 부르는 것과 개발자가 Order 클래스에 담은 것이 서로 다른 개념이라는 걸 3개월 뒤에야 깨닫는다. 결제 도메인의 규칙이 배송 코드 안에 스며들어, 결제 정책을 바꿨더니 배송 로직이 깨진다. 코드는 돌아가지만, 비즈니스를 설명하는 데 아무 도움이 되지 않는다.

Eric Evans는 2003년에 출간한 Domain-Driven Design: Tackling Complexity in the Heart of Software에서 이 문제의 핵심을 짚었다. 소프트웨어의 복잡성은 기술이 아니라 도메인에서 온다. 도메인(Domain)이란 소프트웨어가 해결하려는 비즈니스 문제 영역을 말한다. 이커머스라면 주문·결제·배송·재고가 도메인이고, 병원 시스템이라면 진료·처방·수납이 도메인이다.

DDD(Domain-Driven Design, 도메인 주도 설계)는 이 도메인을 설계의 중심에 놓자는 접근법이다. 프레임워크나 데이터베이스 스키마가 아니라, 비즈니스 규칙과 개념이 코드의 구조를 결정하게 만드는 것이 핵심이다.

기술 중심 사고 vs 도메인 중심 사고

개발을 시작할 때 가장 먼저 떠올리는 게 뭔가? DB 테이블부터 설계하자, REST API 먼저 잡자, 이건 MSA로 가야 하지 않나 — 기술적 관심사가 먼저 온다. 자연스러운 일이다. 개발자니까.

그런데 이 순서가 만드는 문제가 있다. 아래 두 가지 접근을 비교해보자.

기술 중심 사고에서는 orders 테이블을 먼저 만든다.

// 기술 중심: 테이블 구조가 곧 클래스 구조
public class Order {
    private Long id;
    private Long userId;
    private String status;      // "PENDING", "PAID", "SHIPPED"
    private BigDecimal totalAmount;
    private LocalDateTime createdAt;

    // getter, setter 수십 개...
}

이 클래스는 테이블의 거울이다. status는 문자열이고, PENDING에서 PAID로 바꾸는 규칙은 OrderService에 들어가 있을 것이다. 주문이 어떤 상태 전이를 허용하는지, 결제 금액이 0원이면 어떻게 되는지 — 비즈니스 규칙은 이 클래스에 흔적도 없다.

도메인 중심 사고는 다르게 시작한다. “주문이란 무엇인가?”부터 묻는다.

// 도메인 중심: 비즈니스 개념이 코드 구조를 결정한다
class Order private constructor(
    val id: OrderId,
    val customer: CustomerId,
    private var status: OrderStatus,
    private val lines: MutableList<OrderLine>,
) {
    fun place() {
        require(lines.isNotEmpty()) { "주문 항목이 비어있으면 주문할 수 없다" }
        require(status == OrderStatus.DRAFT) { "초안 상태에서만 주문 확정이 가능하다" }
        status = OrderStatus.PLACED
    }

    fun totalAmount(): Money =
        lines.fold(Money.ZERO) { acc, line -> acc + line.subtotal() }
}

차이가 보이는가? place() 메서드 안에 비즈니스 규칙이 살아 있다. 빈 주문은 확정할 수 없고, 초안 상태에서만 확정이 가능하다는 것을 코드가 직접 말해준다. status를 외부에서 문자열로 바꿔치기할 수 없고, Money라는 값 객체가 금액 연산을 담당한다.

이것이 DDD가 추구하는 코드 모습이다. 코드를 읽으면 비즈니스가 보이고, 비즈니스 규칙이 바뀌면 도메인 객체를 고치면 된다. 기술 인프라와 비즈니스 로직이 뒤섞이지 않는다.

도메인이란

도메인(Domain)이라는 단어를 좀 더 정확히 잡고 가자. DDD에서 도메인은 소프트웨어가 해결하려는 문제 영역이다. 기술적인 개념이 아니라 비즈니스 개념이다.

이커머스 플랫폼을 만든다면, 전체 도메인은 온라인 쇼핑이다. 이 안에 여러 하위 도메인(Subdomain)이 있다.

핵심 도메인이 중요한 이유는 단순하다. 여기에 집중하지 않으면 비즈니스가 차별화되지 않기 때문이다. 인증은 OAuth 라이브러리를 쓰면 되고, 결제는 PG사 SDK를 붙이면 된다. 하지만 핵심 도메인은 아무도 대신 만들어주지 않는다.

DDD는 이 핵심 도메인에 최고의 설계 역량을 투입하라고 말한다. 모든 하위 도메인에 같은 수준의 설계를 적용할 필요는 없다. 일반 도메인은 기존 솔루션을 가져다 쓰고, 핵심 도메인에 에너지를 집중하는 것이 전략적으로 옳다.

DDD의 핵심 전제

DDD의 모든 실천법은 하나의 전제에서 출발한다.

소프트웨어의 복잡성은 기술이 아니라 도메인에서 온다.

이 전제를 받아들이면 자연스럽게 따라오는 결론이 있다.

첫째, 도메인 전문가와의 협업이 필수다. 개발자 혼자 코드를 짜서 해결할 수 있는 복잡성이 아니다. 비즈니스를 이해하는 사람과 지속적으로 대화하면서 모델을 발전시켜야 한다. 기획서 던져주면 개발합니다 방식으로는 도메인의 진짜 구조를 파악하기 어렵다.

둘째, 코드가 도메인을 반영해야 한다. 기획서에는 주문 취소라고 쓰여 있는데, 코드에서는 updateStatus(CANCELLED)라고 되어 있으면 둘 사이에 간극이 생긴다. 이 간극이 커질수록 코드를 읽어도 비즈니스를 모르겠다는 상태에 도달한다. DDD는 코드의 클래스명, 메서드명, 변수명이 도메인 용어를 그대로 써야 한다고 주장한다.

셋째, 모델은 진화한다. 처음부터 완벽한 도메인 모델을 만들 수 없다. 비즈니스를 이해해가면서, 코드를 작성하면서, 도메인 전문가와 대화하면서 모델이 점점 정교해진다. DDD에서 모델링은 일회성 작업이 아니라 지속적인 활동이다.

전략적 설계와 전술적 설계

DDD는 크게 두 가지 층위의 설계를 다룬다. 전략적 설계(Strategic Design)와 전술적 설계(Tactical Design)다.

flowchart TB
    DDD["DDD<br/>Domain-Driven Design"]

    SD["전략적 설계<br/>Strategic Design"]
    TD["전술적 설계<br/>Tactical Design"]

    DDD --> SD
    DDD --> TD

    UL["유비쿼터스 언어<br/>Ubiquitous Language"]
    BC["바운디드 컨텍스트<br/>Bounded Context"]
    CM["컨텍스트 매핑<br/>Context Mapping"]
    SUB["서브도메인<br/>Subdomain"]

    SD --> UL
    SD --> BC
    SD --> CM
    SD --> SUB

    ENT["엔티티<br/>Entity"]
    VO["값 객체<br/>Value Object"]
    AGG["애그리게이트<br/>Aggregate"]
    REPO["리포지토리<br/>Repository"]
    DS["도메인 서비스<br/>Domain Service"]
    DE["도메인 이벤트<br/>Domain Event"]
    FAC["팩토리<br/>Factory"]

    TD --> ENT
    TD --> VO
    TD --> AGG
    TD --> REPO
    TD --> DS
    TD --> DE
    TD --> FAC

전략적 설계는 큰 그림을 다룬다. 시스템을 어떤 경계로 나눌 것인가, 각 경계 안에서 어떤 언어를 쓸 것인가, 경계 간 관계는 어떻게 맺을 것인가. 팀 구조와 조직 커뮤니케이션까지 영향을 미치는 거시적 설계다.

전술적 설계는 바운디드 컨텍스트 내부를 다룬다. 도메인 모델을 실제 코드로 옮기는 빌딩 블록이다.

전략적 설계 없이 전술적 설계만 적용하면 DDD 패턴을 쓰긴 했는데, 구조가 여전히 엉망이 된다. Entity와 Value Object를 만들었지만 바운디드 컨텍스트 경계가 없어서 결국 모든 것이 하나의 거대한 모델에 뒤섞이는 상황이다. 반대로 전략적 설계만 하고 전술적 설계를 무시하면 그림은 예쁘지만 코드는 절차적으로 남는다.

둘은 톱니바퀴처럼 맞물린다. 전략적 설계가 경계를 잡아주면, 전술적 설계가 그 경계 안에서 풍부한 도메인 모델을 만들어낸다.

DDD가 풀어주는 문제들

DDD를 적용하면 구체적으로 무엇이 달라지는가? 몇 가지 흔한 문제 상황을 보자.

코드를 읽어도 비즈니스를 모르겠다. 기술 중심으로 설계하면 UserService, OrderRepository, PaymentController 같은 클래스는 있지만, “할인 정책은 어디 있지?”, “주문 상태 전이 규칙은 어디서 관리하지?”라는 질문에 답하기 어렵다. 도메인 모델이 명시적이면 비즈니스 규칙이 코드에 살아 있다.

A를 고쳤더니 B가 깨진다. 바운디드 컨텍스트 경계 없이 모든 것이 하나의 모델에 있으면, 결제 로직 변경이 배송 로직에 파급된다. 컨텍스트를 나누면 각 영역이 독립적으로 변경 가능해진다.

개발자와 기획자가 서로 다른 말을 한다. 유비쿼터스 언어가 없으면, 기획서의 주문 확정과 코드의 confirmOrder()가 같은 것인지 다른 것인지 매번 확인해야 한다. 공통 언어를 쓰면 이 변환 비용이 사라진다.

서비스 클래스가 수천 줄이다. 도메인 로직이 서비스 레이어에 몰려 있으면 이른바 빈약한 도메인 모델(Anemic Domain Model)이 된다. 엔티티는 getter/setter만 있는 데이터 주머니이고, 모든 로직은 서비스 클래스에 들어간다. DDD는 도메인 로직을 도메인 객체 안에 둬서, 서비스가 얇아지고 도메인 모델이 풍부해지는 구조를 만든다.

DDD가 모든 프로젝트에 필요한가

아니다. DDD는 복잡한 도메인에 효과적인 접근법이다.

CRUD가 대부분인 관리 시스템, 비즈니스 규칙이 거의 없는 데이터 파이프라인, 프로토타입 수준의 빠른 검증이 필요한 프로젝트에는 과한 설계가 될 수 있다. 주문 하나에 Entity, Value Object, Aggregate, Repository를 전부 만드는 것이 테이블 하나에 CRUD API를 붙이는 것보다 무조건 나은 건 아니다.

DDD가 빛나는 조건은 이렇다.

이 조건에 해당하지 않으면 DDD의 전략적 설계 개념만 가볍게 참고하고, 전술적 패턴은 필요한 부분에만 선택적으로 적용해도 충분하다.

이 시리즈의 구성

이 시리즈는 전체 7편으로, DDD의 전략적 설계부터 전술적 설계까지 단계적으로 다룬다.

제목핵심 내용
1편DDD란도메인 중심 설계의 필요성, 전략적·전술적 설계 개요
2편유비쿼터스 언어와 Bounded Context공통 언어, 컨텍스트 경계
3편Context Mapping컨텍스트 간 관계를 정의하는 패턴들
4편Entity와 Value Object식별성과 동등성
5편Aggregate와 Repository일관성의 경계와 저장
6편Domain Service, Application Service로직의 위치
7편Domain Event와 Anti-Corruption Layer도메인 간 느슨한 연결

1~3편은 전략적 설계를 다루고, 4~7편은 전술적 설계를 다룬다. 전략적 설계를 먼저 다루는 이유는 간단하다. 경계 없이 전술적 패턴만 적용하면, Entity와 Value Object를 아무리 잘 만들어도 결국 하나의 거대한 진흙 덩어리(Big Ball of Mud)에 묻히기 때문이다. 큰 그림을 먼저 잡고, 그 안을 채워 나가는 순서가 자연스럽다.

코드 예시는 Java와 Kotlin을 함께 사용한다. Java는 DDD 커뮤니티에서 가장 널리 쓰이는 언어이고, Kotlin은 data class와 불변 설계가 Value Object와 궁합이 좋아서 DDD 패턴을 표현하기에 적합하다.

핵심 정리

이 편에서 다룬 내용을 정리하면 이렇다.


다음 편에서는 유비쿼터스 언어와 바운디드 컨텍스트를 다룬다. 개발자와 도메인 전문가가 같은 단어를 쓰면서도 서로 다른 것을 떠올리는 문제, 그리고 그 문제를 경계로 풀어내는 방법을 살펴보자.

2편: 유비쿼터스 언어와 Bounded Context


Related Posts

Share this post on:

Comments

Loading comments...


Previous Post
소프트웨어 아키텍처 6편 — 모듈러 모놀리스: 마이크로서비스 전에 해볼 수 있는 것
Next Post
DDD 2편 — 유비쿼터스 언어와 Bounded Context