2025-09-23 20:36

  • 도메인 주도 설계]는 소프트웨어의 복잡성을 관리하기 위해 비즈니스 도메인 자체에 집중하는 개발 접근 방식이다.

  • DDD는 전략적 설계와 전술적 설계라는 두 가지 주요 파트로 구성되며, 이는 각각 비즈니스와 기술을 연결하는 역할을 한다.

  • 성공적인 DDD 도입은 복잡한 비즈니스 문제를 효과적으로 해결하고, 유지보수성이 높으며 확장 가능한 소프트웨어를 만드는 데 기여한다.

도메인 주도 설계 완벽 핸드북 복잡한 소프트웨어의 나침반

소프트웨어 개발의 세계는 끊임없이 변화하고, 그 복잡성은 날이 갈수록 증가한다. 수많은 기능, 예측 불가능한 요구사항, 그리고 거대한 데이터의 흐름 속에서 우리는 어떻게 길을 잃지 않고 목적지에 도달할 수 있을까? 여기, 복잡성의 안개를 걷어내고 소프트웨어의 본질에 집중하도록 안내하는 강력한 나침반이 있다. 바로 **도메인 주도 설계(Domain-Driven Design, DDD)**다.

이 핸드북은 DDD의 세계를 탐험하려는 개발자, 아키텍트, 그리고 기획자들을 위한 완벽한 안내서다. DDD가 왜 탄생했는지, 그 핵심 철학은 무엇인지, 그리고 실제 프로젝트에 어떻게 적용할 수 있는지에 대한 깊이 있는 통찰을 제공할 것이다. 단순히 기술적인 개념을 나열하는 것을 넘어, DDD가 어떻게 팀의 소통 방식을 바꾸고, 비즈니스의 성공에 기여하는지에 대한 이야기까지 담아냈다.


1. 도대체 왜 도메인 주도 설계가 필요한가

소프트웨어 개발 초기, 프로그램은 비교적 단순했다. 데이터베이스에서 정보를 가져와 화면에 보여주는 CRUD(Create, Read, Update, Delete) 작업이 주를 이루었다. 이러한 방식은 간단한 시스템에서는 효과적이었지만, 비즈니스 로직이 점점 복잡해지면서 한계에 부딪혔다.

전통적인 개발 방식의 가장 큰 문제는 기술 중심적 사고였다. 개발자들은 데이터베이스 테이블 구조나 최신 프레임워크에 집중했고, 정작 소프트웨어가 해결해야 할 핵심 비즈니스 문제, 즉 **‘도메인’**은 뒷전으로 밀려나기 일쑤였다. 이는 다음과 같은 문제들을 낳았다.

  • 비즈니스와 개발의 단절: 기획자와 개발자가 사용하는 언어가 달라 소통 비용이 증가하고, 요구사항이 잘못 해석되는 경우가 빈번했다. “고객 등급 상향 조정”이라는 비즈니스 용어가 개발팀 내에서는 “User 테이블의 grade 컬럼 값을 ‘VIP’로 업데이트”라는 기술 용어로 변질되었다.

  • 암묵적 지식의 만연: 비즈니스의 중요한 규칙들이 특정 개발자의 머릿속에만 있거나 코드 깊숙한 곳에 숨겨져 있어, 새로운 사람이 프로젝트에 적응하기 어렵고 유지보수가 힘들어졌다.

  • 스파게티 코드: 비즈니스 로직이 프레젠테이션 계층, 데이터베이스 접근 계층 등 시스템 전반에 흩어져 있어 코드의 응집도가 떨어지고, 작은 변경이 시스템 전체에 예기치 않은 영향을 미치는 ‘스파게티 코드’가 양산되었다.

이러한 혼란 속에서 2003년, 에릭 에반스(Eric Evans)는 그의 저서 “Domain-Driven Design: Tackling Complexity in the Heart of Software”를 통해 새로운 해법을 제시했다. 그것이 바로 도메인 주도 설계(DDD)다. DDD의 핵심 철학은 간단명료하다.

“소프트웨어의 심장은 그것이 사용되는 비즈니스 도메인이다. 개발의 중심에 도메인을 놓고, 도메인 전문가와 개발자가 협력하여 도메인 모델을 정제하고, 이 모델을 코드에 직접 반영하라.”

마치 건물을 지을 때, 최신 공법이나 화려한 자재에만 집중하는 것이 아니라, 그 건물이 ‘어떤 사람들을 위해’, ‘어떤 목적으로’ 사용될 것인지, 즉 건물의 **본질(도메인)**에 집중하는 것과 같다. DDD는 기술 중심의 사고에서 벗어나 비즈니스 문제 해결이라는 소프트웨어 개발의 본질로 돌아가자는 강력한 외침이었다.


2. 도메인 주도 설계의 두 기둥 전략적 설계와 전술적 설계

DDD는 크게 두 가지 파트로 나뉜다. 하나는 숲을 보는 **전략적 설계(Strategic Design)**이고, 다른 하나는 나무를 심는 **전술적 설계(Tactical Design)**다. 이 둘은 서로 긴밀하게 연결되어 있으며, 성공적인 DDD를 위해서는 어느 한쪽도 소홀히 할 수 없다.

2.1. 전략적 설계 숲을 보고 경계를 정하다

전략적 설계는 전체 비즈니스 도메인을 이해하고, 거대한 시스템을 관리 가능한 여러 부분으로 나누는 과정에 집중한다. 마치 전쟁에서 전체 전장을 분석하고, 각 부대의 작전 구역을 나누는 것과 같다.

Ubiquitous Language (보편 언어)

전략적 설계의 가장 핵심적인 개념은 보편 언어다. 이는 프로젝트에 참여하는 모든 사람(도메인 전문가, 개발자, 기획자, QA 등)이 공통으로 사용하는 언어 체계를 만드는 것을 의미한다.

  • 왜 필요한가?: 앞서 언급했듯, 소통의 단절은 프로젝트 실패의 주된 원인이다. “고객”이라는 단어가 기획자에게는 ‘서비스를 이용하는 사람’을 의미하지만, 개발자에게는 ‘User 테이블의 한 행’으로 인식된다면 혼란이 발생한다. 보편 언어는 이러한 용어의 모호함을 제거하고 모두가 동일한 개념을 공유하게 한다.

  • 어떻게 만드나?: 도메인 전문가와의 끊임없는 대화를 통해 만들어진다. 회의, 문서, 심지어 코드의 변수명이나 클래스명까지 모두 이 보편 언어를 사용해야 한다. 예를 들어, 온라인 쇼핑몰에서 ‘주문’이라는 행위를 논의한다면, ‘고객이 상품을 선택하고 결제를 완료하여 주문을 생성한다’와 같이 명확하게 정의하고, 코드에서도 Order, Customer, Product와 같은 용어를 일관되게 사용한다.

Bounded Context (경계가 있는 컨텍스트)

거대한 비즈니스 도메인은 하나의 단일 모델로 표현하기 어렵다. 예를 들어, ‘상품’이라는 단어는 영업팀에서는 ‘판매 가격’과 ‘프로모션 정보’가 중요하지만, 물류팀에서는 ‘무게’, ‘부피’, ‘창고 위치’가 더 중요하다. 이처럼 같은 용어라도 어떤 맥락(Context)에 있느냐에 따라 의미와 역할이 달라진다.

Bounded Context는 바로 이 모델이 적용되는 명확한 경계를 설정하는 것이다. 각 Bounded Context는 자신만의 보편 언어와 도메인 모델을 가지며, 다른 Context와는 독립적으로 개발될 수 있다. 이는 마이크로서비스 아키텍처(MSA)에서 각 서비스를 나누는 핵심적인 기준이 된다.

컨텍스트’상품’의 의미주요 관심사
제품 카탈로그 컨텍스트고객에게 보여줄 상품 정보상품명, 이미지, 설명, 가격
재고 관리 컨텍스트창고에 보관된 물리적 상품재고 수량, 입출고 기록, 위치
주문 처리 컨텍스트고객이 구매한 상품 항목주문 수량, 상품 가격, 할인

Context Map (컨텍스트 맵)

Bounded Context들을 정의했다면, 이들 사이의 관계를 명확히 해야 한다. 컨텍스트 맵은 Bounded Context 간의 관계를 시각적으로 표현한 지도로, 팀 간의 협업 방식을 결정하는 중요한 도구가 된다.

  • Shared Kernel (공유 커널): 두 개 이상의 컨텍스트가 모델의 일부를 공유. 긴밀한 협업이 필요.

  • Customer-Supplier (고객-공급자): 한쪽 컨텍스트(고객)가 다른 쪽(공급자)에 의존. 공급자는 고객의 요구사항을 고려해야 함.

  • Conformist (준수자): 고객 컨텍스트가 공급자 컨텍스트의 모델을 그대로 따름.

  • Anti-Corruption Layer (ACL, 부패 방지 계층): 외부 시스템과의 연동 시, 우리 시스템의 모델이 외부 모델에 의해 오염되는 것을 막기 위한 번역 계층을 둠. 레거시 시스템과 연동할 때 특히 유용하다.

전략적 설계는 이처럼 거시적인 관점에서 시스템의 청사진을 그리는 과정이다. 이를 통해 복잡한 도메인을 효과적으로 분해하고, 팀 간의 책임과 협업 방식을 명확히 정의할 수 있다.

2.2. 전술적 설계 나무를 심고 가꾸다

전략적 설계가 시스템의 구조를 잡는 과정이라면, 전술적 설계는 각 Bounded Context 내부의 도메인 모델을 구체적으로 어떻게 구현할 것인가에 대한 이야기다. 이는 코딩과 직접적으로 연결되는 구체적인 설계 패턴과 원칙들로 구성된다.

Building Blocks (구성 요소)

전술적 설계는 몇 가지 핵심적인 구성 요소(Building Blocks)를 사용하여 도메인 모델을 표현한다.

  1. Entity (엔티티)

    • 정의: 고유한 식별자(ID)를 가지며, 생성부터 소멸까지의 생명주기 동안 연속성을 유지하는 객체. 속성이 바뀌어도 식별자가 같으면 같은 객체로 인식된다.

    • 비유: ‘사람’은 이름이나 주소가 바뀌어도 주민등록번호라는 고유 식별자를 통해 동일인으로 인식된다. 여기서 ‘사람’이 엔티티, ‘주민등록번호’가 식별자다.

    • 예시: Order (주문 ID), Customer (고객 ID), Product (상품 ID)

  2. Value Object (VO, 값 객체)

    • 정의: 식별자를 갖지 않고, 속성 값 자체로 자신을 표현하는 객체. 불변(Immutable) 객체로 만드는 것이 일반적이다.

    • 비유: ‘주소’는 ‘서울시 강남구 테헤란로 123’이라는 값 자체가 중요하다. 만약 ‘124’로 바뀌면 그것은 더 이상 같은 주소가 아니다.

    • 예시: Money (금액과 통화), Address (주소), Color (색상)

    • 장점: 값 자체에 비즈니스 로직을 포함시킬 수 있다. 예를 들어, Money 객체는 add()isGreaterThan() 같은 메서드를 가질 수 있어, 단순히 원시 타입(e.g., int)을 사용하는 것보다 모델을 훨씬 풍부하게 만든다.

  3. Aggregate (애그리거트)

    • 정의: 관련된 엔티티와 값 객체들을 하나로 묶은 군집. 데이터 변경의 일관성을 유지하는 단위가 된다.

    • 비유: ‘자동차’라는 애그리거트는 ‘엔진’, ‘바퀴’, ‘핸들’ 등 여러 부품으로 구성된다. 우리는 ‘바퀴’를 개별적으로 조작하지 않고, ‘자동차’를 통해 운전한다.

    • 핵심 규칙:

      • 하나의 루트(Root) 엔티티: 애그리거트에는 대표 엔티티인 ‘루트’가 존재한다.

      • 경계: 애그리거트 외부에서는 루트 엔티티를 통해서만 내부에 접근할 수 있다.

      • 트랜잭션 단위: 하나의 트랜잭션에서는 반드시 하나의 애그리거트만 수정해야 한다.

    • 예시: Order 애그리거트는 Order (루트 엔티티), OrderLine (주문 항목, 엔티티), ShippingAddress (배송지 주소, 값 객체)를 포함할 수 있다. 주문 취소는 Order 객체를 통해서만 가능하다.

  4. Repository (리포지토리)

    • 정의: 애그리거트의 저장 및 조회를 담당하는 객체. 도메인 모델과 데이터베이스 인프라를 분리하는 역할을 한다.

    • 비유: 도서관의 사서와 같다. 우리는 책(애그리거트)을 찾을 때 도서관의 모든 서가를 뒤지지 않고, 사서에게 책 제목을 알려주고 찾아달라고 요청한다.

    • 특징: findById, save와 같이 컬렉션과 유사한 인터페이스를 제공하여, 개발자가 복잡한 SQL이나 ORM 기술에 의존하지 않고 도메인 로직에 집중하게 돕는다.

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

    • 정의: 특정 엔티티나 값 객체에 속하지 않는, 여러 도메인 객체를 사용하는 중요한 비즈니스 로직을 수행하는 객체.

    • 비유: 은행에서 ‘송금’ 기능은 보내는 사람의 계좌와 받는 사람의 계좌, 두 개의 엔티티가 필요하다. 이처럼 여러 객체에 걸친 행위를 처리하는 것이 서비스다.

    • 주의: 모든 로직을 서비스에 넣으려는 유혹을 피해야 한다. 가능한 한 엔티티나 값 객체 안에 로직을 배치하고, 그것이 불가능할 때만 도메인 서비스를 사용해야 한다.

  6. Factory (팩토리)

    • 정의: 복잡한 객체 생성 과정을 캡슐화하는 역할. 애그리거트 루트나 엔티티 생성 시, 비즈니스 규칙을 만족하는지 검증하고 일관성 있는 객체를 만드는 책임을 진다.
  7. Domain Event (도메인 이벤트)

    • 정의: 도메인에서 발생한 중요한 사건을 나타내는 객체.

    • 예시: OrderPlaced (주문이 완료됨), PaymentCompleted (결제가 완료됨)

    • 장점: Bounded Context 간의 결합도를 낮추는 데 매우 효과적이다. 주문 컨텍스트는 OrderPlaced 이벤트를 발행하기만 하면 되고, 배송 컨텍스트나 알림 컨텍스트는 이 이벤트를 구독하여 각자의 후속 작업을 처리할 수 있다. 이는 시스템의 유연성과 확장성을 크게 향상시킨다.

전술적 설계의 빌딩 블록들은 도메인의 복잡한 규칙과 프로세스를 코드 상에 명확하고 풍부하게 표현할 수 있도록 돕는 강력한 도구들이다.


3. 도메인 주도 설계 실제로 적용하기

개념을 이해하는 것과 실제 프로젝트에 적용하는 것은 다른 차원의 이야기다. DDD를 성공적으로 도입하기 위한 실용적인 접근법을 알아보자.

1단계: 도메인 전문가와 협력하여 보편 언어 구축하기

가장 먼저 해야 할 일은 도메인 전문가와 개발자가 한자리에 모여 대화하는 것이다. 화이트보드에 그림을 그리고, 용어를 정의하며, 비즈니스 프로세스를 함께 그려나가야 한다. 이 과정에서 발견된 명사, 동사, 규칙들이 보편 언어의 기초가 된다. 이벤트 스토밍(Event Storming)과 같은 워크숍 기법은 이 과정에 매우 유용하다.

2단계: Bounded Context 식별 및 컨텍스트 맵 그리기

전체 비즈니스 영역을 탐색하며 논리적인 경계를 찾는다. 조직의 팀 구조, 업무 흐름, 사용하는 용어의 차이 등이 Bounded Context를 나누는 중요한 단서가 될 수 있다. 각 컨텍스트를 정의하고, 컨텍스트 맵을 통해 그들 간의 관계를 명확히 하여 전체 시스템의 아키텍처를 구상한다.

3단계: 핵심 Bounded Context 내부 모델링

모든 Bounded Context를 한 번에 완벽하게 모델링할 필요는 없다. 비즈니스적으로 가장 중요하고 복잡한 **핵심 도메인(Core Domain)**에 집중한다. 전술적 설계의 빌딩 블록(엔티티, 값 객체, 애그리거트 등)을 사용하여 도메인 모델을 구체화한다. 이 과정에서 도메인 전문가의 피드백은 필수적이다.

  • 애그리거트 설계 Tip: 일관성 규칙을 중심으로 묶는다. “주문 항목이 추가되면 총 주문 금액이 변경되어야 한다”와 같은 규칙이 있다면, OrderOrderLine은 같은 애그리거트에 속할 가능성이 높다. 애그리거트는 가능한 작게 유지하는 것이 좋다.

4단계: 코드로 모델 구현하기

정의된 모델을 코드로 옮긴다. 이때 중요한 것은 도메인 계층의 순수성을 지키는 것이다. 도메인 모델은 특정 프레임워크나 데이터베이스 기술에 의존해서는 안 된다. 순수한 비즈니스 로직만을 담고 있어야 테스트하기 쉽고, 유연하며, 이해하기 쉬운 코드가 된다.

일반적으로 DDD는 계층형 아키텍처(Layered Architecture)나 헥사고날 아키텍처(Hexagonal Architecture)와 함께 사용된다.

  • User Interface (UI) Layer: 사용자 인터페이스, API 엔드포인트 등을 담당.

  • Application Layer: 유스케이스를 오케스트레이션. 트랜잭션 관리, 리포지토리 호출, 도메인 서비스 실행 등. 비즈니스 로직을 직접 포함하지 않음.

  • Domain Layer: DDD의 심장. 엔티티, 값 객체, 도메인 서비스 등 순수한 도메인 모델이 위치.

  • Infrastructure Layer: 데이터베이스, 메시징 큐, 외부 API 연동 등 기술적인 세부 사항을 담당.


4. 도메인 주도 설계를 넘어서 심화 이야기

DDD는 단순히 몇 가지 패턴을 적용하는 것이 아니다. 그것은 사고방식의 전환이며, 지속적인 탐구 과정이다.

CQRS (Command and Query Responsibility Segregation)

전통적인 시스템에서는 데이터를 변경하는 작업(Command)과 조회하는 작업(Query)을 동일한 모델과 데이터베이스로 처리했다. 하지만 복잡한 시스템에서는 이 두 가지 요구사항이 상충하는 경우가 많다. CQRS는 이 둘의 책임을 명확히 분리하는 패턴이다.

  • Command 모델: 상태 변경에 집중. DDD의 애그리거트 모델을 사용. 정규화된 데이터베이스에 적합.

  • Query 모델: 데이터 조회에 최적화된 모델. UI 화면에 필요한 데이터만 담은 DTO(Data Transfer Object) 형태. 비정규화된 읽기 전용 데이터베이스(Read Model)를 사용할 수 있음.

CQRS는 시스템의 성능과 확장성을 크게 향상시킬 수 있지만, 구조가 복잡해지므로 신중하게 도입해야 한다.

Event Sourcing (이벤트 소싱)

일반적으로 우리는 시스템의 ‘현재 상태’만을 데이터베이스에 저장한다. 이벤트 소싱은 이와 달리, 상태를 변경시키는 모든 이벤트를 시간 순서대로 저장하는 방식이다. 현재 상태는 이 이벤트들을 처음부터 재현(replay)함으로써 계산해낼 수 있다.

  • 예시: 은행 계좌의 현재 잔액 ‘10,000원’만 저장하는 대신, ‘계좌 개설(+0)’, ‘입금(+50,000)’, ‘출금(-40,000)‘과 같은 모든 거래 이벤트를 기록하는 것.

  • 장점: 시스템에서 일어난 모든 일을 추적할 수 있어 강력한 감사(Audit) 기능을 제공하며, 특정 시점의 상태를 쉽게 복구할 수 있다. CQRS와 결합하면 매우 강력한 아키텍처를 구축할 수 있다.


결론 소프트웨어 장인 정신으로의 회귀

도메인 주도 설계는 모든 프로젝트를 위한 만병통치약이 아니다. 단순한 CRUD 애플리케이션에 DDD의 모든 개념을 적용하는 것은 과도한 설계(Over-engineering)일 수 있다. DDD는 복잡성이 높은 핵심 도메인을 다룰 때 그 진정한 가치를 발휘한다.

DDD의 여정은 쉽지 않다. 도메인 전문가와의 긴밀한 협업, 지속적인 모델링과 리팩토링, 그리고 새로운 패턴에 대한 학습이 필요하다. 하지만 이 길의 끝에서 우리는 기술의 노예가 아닌, 비즈니스 가치를 창출하는 진정한 소프트웨어 장인이 될 수 있을 것이다. 복잡한 소프트웨어의 망망대해에서 길을 잃었다면, 이제 도메인 주도 설계라는 나침반을 꺼내 들 시간이다.