Tactical Design(전술적 설계)

Tactical DDD 는 전략적 설계로 정의된 Bounded Context 내에서 도메인 모델을 실제 코드 구조로 구현하는 단계이다. 이 단계에서는 Entity(엔터티), Value Object(값 객체), Aggregate(애그리거트), Domain Service(도메인 서비스), Repository(저장소), Domain Event(도메인 이벤트) 같은 전술 패턴을 적용하여 강결합된 도메인 논리를 비즈니스 규칙에 맞게 내부적으로 캡슐화하고, 데이터 일관성을 유지한다. 또한 해당 패턴들로 모듈 경계를 명확히 하고 객체 간 관계를 구조화하여 유지보수성과 확장성을 확보한다.

핵심 개념

개념정의역할실무 구현 요소
Entity (엔터티)고유한 식별자를 가지며 상태와 생명 주기를 관리하는 객체동일성은 ID 로 판단, 상태 변경 가능- ID 필드 (UUID 등)
- 상태 변경 메서드 (changeXXX)
- 비즈니스 불변식 검증
Value Object (값 객체)식별자 없이 속성 값으로 동일성 판단, 불변 객체도메인 개념 표현, 엔터티의 속성 구성- 생성자 내 유효성/불변성 검증
- equals, hashCode 구현
- 수정 불가 구조
Aggregate & Aggregate Root일관성을 유지하는 객체 클러스터, 외부 진입은 루트만 허용트랜잭션 범위 및 불변성 보장 단위- 내부 참조는 루트 통해서만
- 루트에 상태 변경 메서드 집중
- 비즈니스 규칙 캡슐화
Domain Service상태 없는 객체, 엔터티/VO 에 귀속되지 않는 도메인 로직 수행도메인 객체 간 협력 로직 수행- 명령형 도메인 메서드 정의
- 도메인 계층에 위치
- 외부 의존 최소화
RepositoryAggregate 단위의 영속성 추상화도메인 로직과 영속 기술 분리- save, findById, delete
- ORM 연동 (SQLAlchemy, TypeORM 등)
- 인터페이스 분리
Domain Event상태 변화 알림 및 비동기 트리거 객체사이드이펙트 분리 및 이벤트 기반 연동- 이벤트 클래스 (OrderCreatedEvent 등)
- 퍼블리셔/리스너 구조
- 후속 처리 핸들러 구현
Factory복잡한 객체 생성을 외부로 분리하는 패턴생성 책임 분리, 일관된 생성 방식 제공- 정적 팩토리 메서드 (create)
- 빌더 패턴 적용
- 생성 과정 불변성 보장
Specification비즈니스 규칙을 객체화해 재사용 가능하게 함조건 조합 및 검증 로직 분리- isSatisfiedBy(entity) 메서드
- AND/OR/NOT 조합 지원
- 리포지토리 조건 필터로 활용
Ubiquitous Language도메인 전문가와 개발자 간 공통 언어용어 명확화 및 코드 일관성 강화- 클래스/메서드에 도메인 용어 반영
- 주석 및 문서화에 반영
Bounded Context도메인 모델이 유효한 논리적 경계모델 중복/충돌 방지, 통합 전략 기반- 모듈/마이크로서비스 경계 설정
- Context Mapping 활용
(ACL, Shared Kernel 등)

추가 실무 고려 요소

요소설명
트랜잭션 경계Aggregate 단위에서 관리. DB 트랜잭션과 일치시켜 데이터 정합성 보장.
계층 분리 (Layered Architecture)Domain / Application / Infrastructure / Interface 계층 분리
의존성 역전 (Dependency Inversion)도메인이 외부 계층에 의존하지 않도록 인터페이스 추상화
불변성 보장 (Invariant Enforcement)Entity, Value Object, Aggregate 내부에서 규칙을 보호하고 검증

목적 및 필요성

주요 목적:

  1. 비즈니스 복잡성 관리: 복잡한 비즈니스 규칙을 명확하게 표현
  2. 유지보수성 향상: 변경 요구사항에 유연하게 대응
  3. 도메인 지식 보존: 비즈니스 로직을 코드에 명시적으로 표현
  4. 협업 개선: 비즈니스 전문가와 개발자 간 소통 원활화

필요성:

주요 기능 및 역할

  1. 도메인 모델 구체화: 추상적인 비즈니스 개념을 구현 가능한 코드로 변환
  2. 비즈니스 규칙 캡슐화: 도메인 로직을 적절한 객체에 배치
  3. 데이터 일관성 보장: Aggregate 를 통한 트랜잭션 경계 관리
  4. 관심사 분리: 도메인 로직과 기술적 관심사의 명확한 분리

특징

핵심 원칙

전술 설계 (Tactical Design) 는 개념적 도메인 모델을 코드 수준으로 구체화하는 단계로, 비즈니스 요구사항을 도메인 모델로 추상화하고, 이를 기반으로 소프트웨어를 구현하며, 구현 결과에 대한 피드백을 통해 모델을 지속적으로 개선하는 것이다.
Tactical Design 은 모델 주도 설계의 실천적 구현 단계로, 도출된 도메인 모델을 소프트웨어 구조와 코드로 구체화한다. 즉, 모델 주도 설계가 " 무엇을 모델링할 것인가 " 에 집중한다면, Tactical Design 은 " 어떻게 모델을 코드로 구현할 것인가 " 에 집중한다.

  1. Bounded Context 내에서 실행

    • 전술적 설계는 전략적 설계로 이루어진 경계 (bounded context) 내부에서, 도메인 모델을 세밀하게 구성하고 구현하는 단계이다.
    • 각 컨텍스트는 독립된 모델과 언어 (Ubiquitous Language) 를 갖고, 모델이 내부적으로 일관성을 유지해야 한다.
  2. 도메인 주체의 실체화

    • Entity, Value Object, Aggregate 같은 DDD 빌딩 블록을 코드로 구체화한다.
    • Entity: 고유 ID 기반의 라이프사이클을 갖는 객체.
    • Value Object: 불변이며 값 자체로 정체성이 결정된다.
    • Aggregate: 하나의 루트 엔티티 주도 하에 관련 객체 묶음 구성, 상태 불변성과 비즈니스 규칙 인한 일관성 보장.
  3. 도메인 행동 캡슐화

    • 중요 비즈니스 로직은 엔티티나 값 객체 내부 메서드로 구현하고, 상태 변경은 도메인 객체를 통해 수행한다.
    • 이를 통해 애플리케이션 서비스나 UI 레이어가 아닌 도메인 계층에 로직이 집중된다.
  4. 인프라와 분리된 저장소 인터페이스

    • Repository 패턴을 통해 도메인 모델의 저장·조회 책임을 추상화하며, 비즈니스 계층과 데이터 영속화의 종속성을 방지.
  5. 무상태성과 명확한 단일 책임

    • Domain Service: Entity 에 속하지 않는 비즈니스 로직을 수행하며, 상태를 유지하지 않고 명확한 비즈니스 역할만 수행한다.
    • Factory: 복잡한 도메인 객체 생성 절차를 분리하여 코드 양방향성을 줄이고 SRP(Single Responsibility Principle) 준수.
  6. 도메인 이벤트를 통한 의사소통

    • Domain Event는 시스템 내 중요한 상태 변화를 캡쳐하여 느슨한 결합 방식으로 구성요소 간 상호작용을 원활히 한다.
  7. 모듈화 및 구조 정리

    • Module 또는 패키지를 통해 관련 도메인 개념 (Entity, VO 등) 을 비즈니스 단위로 그룹화하며, 탐색성과 유지보수성을 향상시킨다.
  8. 반복적 모델 - 코드 피드백 루프

    • 전술 설계는 반복적인 탐색–구현–리팩터링 사이클을 포함하며, 이는 모델과 코드의 동기화를 유지해 도메인 변경을 빠르게 수용할 수 있게 한다.
graph TD
    A[비즈니스 요구사항] --> B[도메인 모델]
    B --> C[구현 코드]
    C --> D[피드백]
    D --> A
    
    B --> E[Entity]
    B --> F[Value Object]
    B --> G[Aggregate]
    B --> H[Domain Service]

구조 및 아키텍처

graph TB
    %% 계층별 그룹 정의
    subgraph "Application Layer"
        AL[Application Service]
        AH[Application Handler]
    end

    subgraph "Domain Layer"
        AG[Aggregate Root]
        E[Entity]
        VO[Value Object]
        DS[Domain Service]
        REPOI[Repository Interface]
        DE[Domain Event]
        F[Factory]
    end

    subgraph "Infrastructure Layer"
        R[Repository Implementation]
        DB[(Database)]
        EH[Event Handler]
        ES[External System]
    end

    %% 흐름 정의
    AL --> AG
    AL --> DS
    AL --> REPOI
    AH --> DS

    AG --> E
    AG --> VO
    AG --> DE
    F --> AG
    REPOI --> R
    R --> DB

    EH --> DE
    ES --> DS

    %% 느슨한 결합 표현
    AL -.-> DE
    AG -.-> REPOI
    REPOI -.-> R

핵심 도메인 빌딩 블록

핵심 도메인 빌딩 블록은 도메인 주도 설계 (DDD, Domain-Driven Design) 에서 복잡한 비즈니스 도메인을 효과적으로 소프트웨어로 구현하기 위해 사용하는 전술적 설계 (Tactical Design) 의 주요 구성 요소를 의미한다.
이 빌딩 블록들은 도메인 모델을 코드로 구체화할 때 일관성과 유지보수성을 높여주며, 도메인 로직의 명확한 분리와 재사용성을 보장한다.

핵심 도메인 빌딩 블록의 목적은 다음과 같다:

구분구성요소기능역할특징
핵심Entity (엔티티)고유 식별자를 기반으로 상태를 가지며 변경 가능도메인 개념의 실체화, 상태 추적 및 생명주기 관리ID 기반 동일성, 가변성
Value Object (값 객체)불변의 값으로 도메인 속성 표현동일성 판단, 개념 명확화, 사이드 이펙트 없는 사용불변성, 값 비교 기반 동일성
Aggregate Root (집합체 루트)관련 엔티티와 값 객체 묶음의 트랜잭션 및 일관성 경계 책임Aggregate 내부 일관성 보장, 외부 노출 지점루트 외부 접근 제한, 트랜잭션 단위 관리
Repository (저장소 인터페이스)도메인 객체의 저장/조회 인터페이스도메인 계층과 인프라 계층 분리, 데이터 접근 추상화인터페이스 기반, 테스트 대체 용이
보완Domain Service (도메인 서비스)여러 도메인 객체에 걸친 도메인 연산 처리Entity 에 귀속되지 않는 핵심 비즈니스 로직 수행무상태, SRP(단일 책임 원칙) 준수
Factory (팩토리)복잡한 객체 (예: Aggregate) 생성 책임 분리도메인 객체 생성의 불변식 및 유효성 보장생성 책임 위임, 도메인 초기화 캡슐화
Domain Event (도메인 이벤트)도메인 내 중요한 사건을 객체로 표현상태 변화의 표현, 모듈 간/시스템 간 비동기 통신 지원이벤트 중심 구조, 느슨한 결합
Entity (엔티티)
Value Object (값 객체)
Aggregate Root (집합체 루트)
Repository (저장소 인터페이스)
Domain Service (도메인 서비스)
Factory (팩토리)
Domain Event (도메인 이벤트)

기타

구분구성요소기능역할특징
인프라Repository Implementation (저장소 구현체)실제 영속성 (DB) 과의 연결을 담당하는 클래스도메인 인터페이스를 구현하여 DB 접근구현체 분리, Test Double 사용 가능
Database (데이터베이스)상태 영속성 저장소도메인 객체의 영속 데이터 저장관계형/NoSQL 모두 가능
Event Handler (이벤트 핸들러)Domain Event 를 구독하고 후속 처리 로직 수행알림, 로깅, 비동기 프로세싱 등 외부 트리거비동기 메시지 처리, 이벤트 기반 연동
External System (외부 시스템)외부 API, 메시지 브로커, 타 시스템 등과 통합도메인 서비스 또는 핸들러에서 호출되는 외부 구성 요소외부 의존, 네트워크 경계 명확화
응용Application Service (앱 서비스)유스케이스 단위의 비즈니스 흐름 조율도메인 계층 조정, 트랜잭션 경계 제어도메인 객체 직접 로직 수행하지 않음
Application Handler (앱 핸들러)명령/이벤트/요청을 처리하고 Application Service 호출입력값 유효성 검사, 흐름 제어, 트리거 역할경량 계층, 흐름 진입점

구현 기법

기법정의 & 목적주요 특징대표 예시
Guard / Assertion도메인 객체의 생성 및 변경 시 ** 불변식 (invariant)** 과 ** 사전조건 (precondition)** 을 코드에서 직접 검사- 오류를 빠르게 감지하고
- 일관성, 신뢰성 확보
수량 ≥ 1 검증, 이메일/우편번호 포맷 확인
Specification복잡한 비즈니스 규칙을 별도 객체로 추출하여 조합과 재사용이 가능하도록 구성- 규칙 조합 가능 (AND/OR/NOT)
- 테스트 및 유지 보수 용이
OrderEligibleSpec, UserActiveSpec
Optimistic Locking버전 (version) 필드 기반으로 동시성 충돌을 감지 및 처리하는 낙관적 잠금 기법- 충돌 시 예외와 재시도 유도
- 데이터 일관성 유지
JPA @Version, updated_at 기반 충돌 처리
CQRS** 읽기 (Query)** 와 쓰기 (Command) 모델을 분리해 성능·확장성·유지보수성을 극대화- 병목 해소 및 책임 분리
- CQRS 와 Event Sourcing 동시 적용 가능
OrderWriteService, OrderReadService, Event Sourcing 연동
Domain Event도메인 내부의 중요한 상태 변화를 이벤트 객체로 표현하여 비동기, 모듈 간 느슨한 결합을 지원- 불변 이벤트
- 퍼블리시/구독 모델
- 추적성 보장
OrderPlacedEvent, UserRegisteredEvent
Essence ObjectUI(form) ↔ 도메인 간 데이터 전송과 유효성 검증용, 가벼운 DTO 대용 객체- 도메인 순수성 유지
- 입력 검증과 바인딩 전용
RegistrationEssence, ProfileUpdateEssence

장점

항목설명
비즈니스 로직 명확화도메인 개념과 규칙을 코드에서 명시적으로 표현하여 비즈니스 의도가 명확하게 전달됨
도메인 캡슐화Aggregate 내부에서만 상태 변경을 허용하여 외부와의 불필요한 결합 방지
일관성과 무결성 보장Aggregate Root 가 트랜잭션 및 규칙을 강제하여 데이터 일관성과 무결성을 유지
테스트 용이성각 도메인 객체가 독립적이며 상태 기반으로 단위 테스트 작성이 수월함
유지보수성 향상Bounded Context 와 계층 구조를 통한 관심사 분리로 변경에 유연하게 대응 가능
확장성과 유연성계층화 아키텍처 및 도메인 중심 설계를 통해 새로운 기능 추가 시 구조적 확장이 쉬움
도메인 지식의 보존핵심 규칙과 로직이 코드 안에 녹아 있어, 문서 없이도 도메인 지식을 전달 가능
협업 효율성 개선유비쿼터스 언어 (Ubiquitous Language) 를 기반으로 이해관계자 간 의사소통이 명확해짐
저장소 추상화로 인한 유연성Repository 를 통한 데이터 저장/조회 추상화로 DB 나 인프라 기술 변경이 쉬움

단점과 문제점 그리고 해결방안

단점

항목설명해결 방안
초기 복잡성패턴과 계층이 많아 간단한 애플리케이션에도 구조가 복잡해짐핵심 도메인 범위부터 단계적으로 적용, KPI 기반 코드 리뷰 통한 확장 여부 결정
진입 장벽DDD, 패턴, 계층 이해에 시간이 소요됨단계적 교육, 코드 템플릿/문서 제공
성능/추상화 비용계층, 추상화, 이벤트 등의 도입으로 성능 오버헤드 발생 가능성능 모니터링, 병목 부분 CQRS/캐싱/직접 접근 등 선택적 최적화
과도한 엔지니어링단순 CRUD 에도 무분별한 설계 적용 시 비용 대비 실익 미흡비즈니스 가치/복잡도 기반 판단, 적절한 경계선 내에서 패턴 사용

문제점 및 해결방안

모델 구현 부실
문제점원인영향탐지 및 예방 방법해결 방법 및 기법
Anemic Domain Model로직을 Entity 에 넣지 않고 서비스 계층에만 몰아둠도메인 객체가 빈 껍데기처럼, 의미 상실코드 리뷰, 모델 테스팅도메인 메서드 중심 설계, 엔티티로 로직 이동, Specification 사용
규칙 누락비즈니스 규칙을 코드에 반영하지 않음일관성 저해, 예외/버그 위험테스트&코드 분석, 코드 리뷰Guard/Assertion 도입, Specification 정리
Aggregate 설계 오류
문제점원인영향탐지 및 예방 방안해결 방법 및 기법
Aggregate 경계 불명확Transaction 경계 및 도메인 이해 부족데이터 불일관성, 복잡도 증가도메인 워크숍, 설계 검토Aggregate 재정의, CQRS/Event-driven 설계
Aggregate 지나치게 큼루트가 너무 많은 책임과 멤버를 포함성능 저하, 테스트 어려움코드 프로파일링, 복잡도 분석작은 Aggregate 로 분할, CQRS, 서비스로 일부 로직 분리
저장소 패턴 오용
문제점원인영향탐지 및 예방해결 방법 및 기법
Repository 남용모든 엔티티마다 저장소를 생성코드베이스 복잡화Repository 수 검토, 코드 리뷰Aggregate Root 에만 저장소 제공, 불필요한 클래스 제거
ORM- 도메인 매핑 불일치ORM 이 도메인 경계와 자연스럽게 맞지 않음모델 왜곡 및 단순화ORM 구조 분석, 스키마 리뷰Mapper 계층 도입, ORM 비즈니스 로직 분리

도전 과제

구분도전 과제원인 또는 배경영향 또는 리스크해결 방안 및 기법
기술적Aggregate 적절 분리트랜잭션 범위, 경계 설계 미흡데이터 일관성 저하, 설계 오류트랜잭션 중심 도메인 분석, CQRS, Aggregate 크기 가이드라인
이벤트 일관성 관리도메인 이벤트/통합 이벤트 구분 어려움외부 시스템과 비동기 동기화 문제이벤트 설계 표준화, Event Sourcing, Outbox 패턴
성능 및 트랜잭션 문제과도한 객체 간 연결, 트랜잭션 충돌응답 지연, Lock 경쟁CQRS, 캐싱, Async 처리, Bulk 처리
테스트 코드 복잡화계층/패턴 많아짐에 따른 단위 테스트 구성 어려움테스트 커버리지 감소, 유지보수 저하전용 테스트 DSL 도입, Test Fixture 구성, 통합 테스트 중심 구성
시각화 및 자동화 도구 부족설계 모델과 코드 싱크 자동화 도구 부족경계 혼란, 설계 문서 누락모델 시각화 도구 도입 (ex. Structurizr), Mermaid/PlantUML 기반 문서화
레거시 통합기존 시스템과 도메인 모델 충돌통합 복잡도, 코드 분기 증가Anti-Corruption Layer, Adapter, 점진적 전환
조직적팀 역량 및 경험 부족DDD 와 Tactical 패턴 학습 경험 부족적용 실패, 설계 오류워크숍, 도메인 세션, 코드리뷰 기반 멘토링
도메인 전문가와 협업 부족유비쿼터스 언어 미활용, 도메인 공유 미흡요구사항 누락, 모델 부정확성Event Storming, 공유 문서, 정기 협업 세션
경계 정의의 모호성도메인 간 책임과 기능 분리 기준 불명확책임 혼란, 모듈 간 결합도 증가Bounded Context 명세 정의, Context Map 활용
전략적대규모 시스템 적용협업 인원 증가, 도메인 다층화일관성 유지 어려움, 커뮤니케이션 비용 증가문서 표준화, 계층적 도메인 전략, 공통 도메인 공유 체계 수립

실무 사용 예시

도메인사용 목적적용 패턴 / 기술기대 효과
이커머스주문/결제 관리Order Aggregate, Payment Value Object트랜잭션 일관성, 가격 정책 및 결제 정보 캡슐화
금융 서비스계좌 관리Account Entity, Money Value Object상태 추적, 환율 및 금액 로직 도메인화
모빌리티 플랫폼예약/취소 처리Reservation Aggregate, Domain EventCQRS + Event Sourcing 활용 가능, 비동기 확장성 확보
의료 시스템환자 정보 기록Patient Aggregate, MedicalRecord Entity데이터 정합성 보장, 추적 가능한 변경 이력 관리
물류/배송위치/상태 추적Shipment Aggregate, Location Value Object위치 정보 정합성 유지, 실시간 상태 추적 가능

활용 사례

사례 1: 이커머스 주문 시스템

시스템 구성:

활용 워크플로우:

  1. 주문 요청 → OrderService
  2. OrderRoot 생성 → 주문 VO/Entity 구성
  3. 결제 로직 Domain Service 호출
  4. OrderRoot.confirm() → 상태 transitioning
  5. Repository.save() → Inventory 업데이트 + Domain Event
  6. Domain Event → Integration Event 발행

차이 분석: 기존 트랜잭션 스크립트 방식과 비교 시, 도메인 중심 모델 설계로 로직 재사용성과 유지보수성 우위

sequenceDiagram
    participant API
    participant OrderService
    participant OrderAggregate
    participant PaymentService
    participant OrderRepository
    participant EventPublisher

    API->>OrderService: createOrder(command)
    OrderService->>OrderAggregate: Order.create(…)
    OrderAggregate->>PaymentService: verifyPayment()
    PaymentService-->>OrderAggregate: OK
    OrderAggregate->>OrderRepository: save(order)
    OrderRepository->>EventPublisher: publish(OrderPlacedEvent)

사례 2: 전자상거래 주문 관리 시스템

시스템 구성:

시스템 구성 다이어그램:

graph LR
    subgraph "Order Aggregate"
        O[Order Root]
        OI[OrderItem]
        M[Money VO]
    end
    
    subgraph "Customer Aggregate"
        C[Customer Root]
        A[Address VO]
    end
    
    subgraph "Product Aggregate"
        P[Product Root]
        PR[Price VO]
    end
    
    O --> OI
    OI --> M
    O -.-> C
    OI -.-> P
    C --> A
    P --> PR

Workflow:

  1. 고객이 상품을 장바구니에 추가
  2. 주문 생성 시 Order Aggregate 생성
  3. 각 상품에 대해 OrderItem Entity 생성
  4. 총 금액 계산 및 Money Value Object 생성
  5. 결제 처리 및 주문 상태 업데이트
  6. 재고 차감 및 배송 프로세스 시작

역할:

기존 방식과의 차이점:

구현 예시

다음은 전자상거래 주문 시스템의 Python 구현 예시:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
from dataclasses import dataclass
from decimal import Decimal
from enum import Enum
from typing import List, Optional
from uuid import uuid4, UUID

# Value Objects
@dataclass(frozen=True)
class Money:
    """금액을 표현하는 값 객체"""
    amount: Decimal
    currency: str = "KRW"
    
    def __post_init__(self):
        if self.amount < 0:
            raise ValueError("금액은 음수일 수 없습니다")
        if len(self.currency) != 3:
            raise ValueError("통화 코드는 3자리여야 합니다")
    
    def add(self, other: 'Money') -> 'Money':
        if self.currency != other.currency:
            raise ValueError("다른 통화는 더할 수 없습니다")
        return Money(self.amount + other.amount, self.currency)
    
    def multiply(self, factor: int) -> 'Money':
        return Money(self.amount * factor, self.currency)

@dataclass(frozen=True)
class ProductId:
    value: str
    
    @classmethod
    def generate(cls):
        return cls(str(uuid4()))

@dataclass(frozen=True)
class OrderId:
    value: str
    
    @classmethod
    def generate(cls):
        return cls(str(uuid4()))

# Entities
class OrderItem:
    """주문 항목 엔티티"""
    def __init__(self, product_id: ProductId, price: Money, quantity: int):
        if quantity <= 0:
            raise ValueError("수량은 0보다 커야 합니다")
        
        self._product_id = product_id
        self._price = price
        self._quantity = quantity
    
    @property
    def product_id(self) -> ProductId:
        return self._product_id
    
    @property
    def price(self) -> Money:
        return self._price
    
    @property
    def quantity(self) -> int:
        return self._quantity
    
    def get_total_price(self) -> Money:
        """주문 항목의 총 금액 계산"""
        return self._price.multiply(self._quantity)
    
    def change_quantity(self, new_quantity: int):
        """수량 변경 (비즈니스 규칙 적용)"""
        if new_quantity <= 0:
            raise ValueError("수량은 0보다 커야 합니다")
        self._quantity = new_quantity

class OrderStatus(Enum):
    PENDING = "pending"
    CONFIRMED = "confirmed"
    SHIPPED = "shipped"
    DELIVERED = "delivered"
    CANCELLED = "cancelled"

# Aggregate Root
class Order:
    """주문 집합체 루트"""
    def __init__(self, order_id: OrderId, customer_id: str):
        self._id = order_id
        self._customer_id = customer_id
        self._items: List[OrderItem] = []
        self._status = OrderStatus.PENDING
        self._domain_events: List = []
    
    @property
    def id(self) -> OrderId:
        return self._id
    
    @property
    def customer_id(self) -> str:
        return self._customer_id
    
    @property
    def status(self) -> OrderStatus:
        return self._status
    
    @property
    def items(self) -> List[OrderItem]:
        return self._items.copy()
    
    def add_item(self, product_id: ProductId, price: Money, quantity: int):
        """주문 항목 추가"""
        if self._status != OrderStatus.PENDING:
            raise ValueError("확정된 주문에는 항목을 추가할 수 없습니다")
        
        # 동일 상품이 이미 있는지 확인
        existing_item = self._find_item_by_product(product_id)
        if existing_item:
            existing_item.change_quantity(existing_item.quantity + quantity)
        else:
            new_item = OrderItem(product_id, price, quantity)
            self._items.append(new_item)
    
    def remove_item(self, product_id: ProductId):
        """주문 항목 제거"""
        if self._status != OrderStatus.PENDING:
            raise ValueError("확정된 주문에서는 항목을 제거할 수 없습니다")
        
        self._items = [item for item in self._items if item.product_id != product_id]
    
    def get_total_amount(self) -> Money:
        """주문 총 금액 계산"""
        if not self._items:
            return Money(Decimal('0'))
        
        total = self._items[0].get_total_price()
        for item in self._items[1:]:
            total = total.add(item.get_total_price())
        return total
    
    def confirm_order(self):
        """주문 확정"""
        if not self._items:
            raise ValueError("빈 주문은 확정할 수 없습니다")
        if self._status != OrderStatus.PENDING:
            raise ValueError("이미 처리된 주문입니다")
        
        self._status = OrderStatus.CONFIRMED
        # 도메인 이벤트 발행
        self._domain_events.append(OrderConfirmedEvent(self._id, self._customer_id))
    
    def cancel_order(self):
        """주문 취소"""
        if self._status in [OrderStatus.SHIPPED, OrderStatus.DELIVERED]:
            raise ValueError("배송된 주문은 취소할 수 없습니다")
        
        self._status = OrderStatus.CANCELLED
        self._domain_events.append(OrderCancelledEvent(self._id, self._customer_id))
    
    def _find_item_by_product(self, product_id: ProductId) -> Optional[OrderItem]:
        """상품 ID로 주문 항목 찾기"""
        for item in self._items:
            if item.product_id == product_id:
                return item
        return None
    
    def get_domain_events(self) -> List:
        """도메인 이벤트 조회"""
        return self._domain_events.copy()
    
    def clear_domain_events(self):
        """도메인 이벤트 클리어"""
        self._domain_events.clear()

# Domain Events
@dataclass
class OrderConfirmedEvent:
    order_id: OrderId
    customer_id: str
    
@dataclass  
class OrderCancelledEvent:
    order_id: OrderId
    customer_id: str

# Repository Interface
from abc import ABC, abstractmethod

class OrderRepository(ABC):
    """주문 저장소 인터페이스"""
    
    @abstractmethod
    def save(self, order: Order) -> None:
        pass
    
    @abstractmethod
    def find_by_id(self, order_id: OrderId) -> Optional[Order]:
        pass
    
    @abstractmethod
    def find_by_customer(self, customer_id: str) -> List[Order]:
        pass

# Domain Service
class OrderService:
    """주문 도메인 서비스"""
    
    def __init__(self, order_repository: OrderRepository):
        self._order_repository = order_repository
    
    def create_order(self, customer_id: str) -> Order:
        """새 주문 생성"""
        order_id = OrderId.generate()
        order = Order(order_id, customer_id)
        return order
    
    def calculate_shipping_cost(self, order: Order) -> Money:
        """배송비 계산 (복잡한 비즈니스 로직)"""
        total_amount = order.get_total_amount()
        
        # 10만원 이상 무료배송
        if total_amount.amount >= Decimal('100000'):
            return Money(Decimal('0'))
        
        # 기본 배송비
        return Money(Decimal('3000'))

# Application Service (사용 예시)
class OrderApplicationService:
    """주문 애플리케이션 서비스"""
    
    def __init__(self, order_repository: OrderRepository, order_service: OrderService):
        self._order_repository = order_repository
        self._order_service = order_service
    
    def place_order(self, customer_id: str, items: List[dict]) -> str:
        """주문 생성 및 처리"""
        # 1. 새 주문 생성
        order = self._order_service.create_order(customer_id)
        
        # 2. 주문 항목 추가
        for item_data in items:
            product_id = ProductId(item_data['product_id'])
            price = Money(Decimal(item_data['price']))
            quantity = item_data['quantity']
            order.add_item(product_id, price, quantity)
        
        # 3. 주문 확정
        order.confirm_order()
        
        # 4. 저장
        self._order_repository.save(order)
        
        # 5. 도메인 이벤트 처리 (실제로는 이벤트 퍼블리셔 사용)
        events = order.get_domain_events()
        for event in events:
            self._handle_domain_event(event)
        
        order.clear_domain_events()
        
        return order.id.value
    
    def _handle_domain_event(self, event):
        """도메인 이벤트 처리"""
        if isinstance(event, OrderConfirmedEvent):
            print(f"주문 {event.order_id.value}가 확정되었습니다")
        elif isinstance(event, OrderCancelledEvent):
            print(f"주문 {event.order_id.value}가 취소되었습니다")

실무에서 효과적으로 적용하기 위한 고려사항 및 주의할 점

구분고려사항주의할 점권장사항
도메인 모델링도메인 경계와 핵심 개념을 명확히 정의기술 중심으로만 도메인 구성 지양도메인 전문가와 협업, 유비쿼터스 언어 활용
패턴 적용 전략Aggregate, Entity, Value Object 등 핵심 패턴의 정확한 이해모든 객체에 패턴 무분별 적용교육 및 문서화 기반 선택적 적용
계층 구조 설계계층별 책임 분리 및 역할 명확화계층 간 의존도 모호하거나 순환 참조 발생계층화 아키텍처 (Layered Architecture) 유지
Repository 설계도메인 중심 인터페이스로 추상화DB 중심의 저장/조회 설계Aggregate Root 단위로 Repository 제공
Aggregate 설계트랜잭션 경계, 일관성 규칙 고려지나치게 큰 Aggregate 는 성능 저하 유발작고 응집력 있는 단위로 구성
성능 최적화Command/Query 분리복잡한 조회를 도메인 내부에서만 처리CQRS, Projection, Read Model 사용
테스트 전략도메인 로직에 대한 단위 테스트 중심통합 테스트만으로 신뢰성 확보 어려움테스트 피라미드 적용 (Unit > Integration)

최적화하기 위한 고려사항 및 주의할 점

항목고려사항 또는 설명주의할 점권장사항
Aggregate 크기Aggregate 는 트랜잭션 경계 단위로 작고 응집력 있게 유지불필요한 관계까지 포함 시 메모리/쿼리 낭비필요한 도메인만 포함, Lazy Loading 전략 적용
트랜잭션 관리Aggregate 단위로 트랜잭션을 제한긴 트랜잭션 → deadlock 가능성CQRS 패턴 도입, 명확한 트랜잭션 경계 정의
이벤트 처리비동기 이벤트 기반은 확장성과 분리도 장점느린 소비자, 데이터 불일치 위험이벤트 스키마 버저닝, 병렬 소비자 구조 설계
지연/즉시 로딩 전략성능에 따라 fetch 전략을 선택적으로 설정N+1 쿼리 발생적절한 FetchType 설정 (JPA), 로딩 시점 제어
ORM 연계 분리도메인 모델과 ORM 분리를 통해 순수 도메인 유지ORM Entity 가 도메인에 침투맵퍼 계층 도입, Clean Architecture 원칙 적용
테스트 전략도메인 로직의 단위 테스트 중심무조건 통합 테스트로 의존성 커짐테스트 더블 (Mock), 병렬 테스트, 테스트 피라미드 적용
복잡성 관리객체 수 증가로 인한 설계 복잡성 증가도메인 오염, 패턴 과용리팩토링 기준 수립, 도메인 설명 문서화
캐싱/메모리 최적화Value Object 는 불변성을 활용한 캐싱이 가능생성 비용 증가객체 풀링, 캐시 라이브러리 활용
CQRS ReadModel 설계Command/Query 분리 시 조회 모델 최적화 필요Projection 모델 없으면 유지보수 어려움Projection Test 가능 모델 정의, Index 설계
Guard vs Validator즉각 실패 (Guard) vs 유효성 체크 (Validator) 구분모든 유효성 체크를 하나로 처리 시 가독성 저하책임 분리, 구체적 예외 명시
모듈화 및 의존성 관리도메인 간 의존성 최소화, 독립성 유지과도한 결합 → 변경 파급인터페이스 분리, Bounded Context 설계
변경 관리 대응성요구사항 변경을 빠르게 반영 가능하도록 설계도메인과 기능 간 강결합도메인 이벤트 설계 기반의 유연한 흐름 구성
모니터링 및 추적성도메인 이벤트 기반으로 비즈니스 상태를 추적 가능기술 로그만 의존 시 도메인 가시성 저하Business Metric, Audit Log 구축

기타 사항

주제와 관련하여 주목할 내용

카테고리주제항목설명
도메인 주도 설계전술적 설계 구성 요소Aggregate도메인 모델의 일관성과 무결성 관리, 트랜잭션 경계 단위 역할
Entity고유 식별자 보유, 상태 변경 가능, 생명주기 관리
Value Object불변 객체, 속성 기반 동등성 판단
Repository vs Factory생성 책임과 저장 책임 분리로 역할 명확화
구현 기법Specification Pattern비즈니스 규칙 캡슐화조건 검증 로직을 조합 가능한 객체로 외부화하여 재사용성과 테스트 용이성 확보
Essence ObjectUI → Domain 데이터 전달DTO 대체 혹은 보완으로 사용자 입력 데이터 구조화 및 유효성 검증
아키텍처 패턴Layered Architecture계층별 책임 분리표현, 응용, 도메인, 인프라 계층으로 분리하여 유지보수성과 확장성 향상
헥사고날 아키텍처Ports and Adapters내부 도메인 로직과 외부 인터페이스를 명확히 분리하는 유연한 아키텍처 구조
CQRSCommand/Query 분리읽기와 쓰기 모델을 분리하여 성능 최적화와 구조 명확화
이벤트 소싱Event Store상태 저장 대신 이벤트 저장 → 상태 재생성 가능
모델링 기법Event Storming도메인 이벤트 기반 모델링업무 흐름 중심의 시각적 도메인 설계 협업 워크숍
테스트 전략TDD (Test-Driven Development)테스트 우선 설계요구사항을 코드로 빠르게 구체화, 도메인 규칙 검증 중심 테스트 유도

주제와 관련하여 반드시 학습해야할 내용

카테고리주제항목설명
객체지향 설계 원리OOP 원칙캡슐화 (Encapsulation), 상속 (Inheritance), 다형성 (Polymorphism)객체 간 책임 분리와 재사용성을 위한 기본 설계 원칙
SOLID 원칙단일 책임, 개방 - 폐쇄, 리스코프 치환, 인터페이스 분리, 의존 역전유지보수성과 확장성을 높이기 위한 객체지향 설계의 5 대 핵심 원칙
소프트웨어 설계 패턴GoF 디자인 패턴Factory, Strategy, Observer 등 23 가지 패턴상황에 맞는 재사용 가능한 구조/행위/생성 패턴을 적용해 문제를 해결하는 설계법
소프트웨어 아키텍처계층화 아키텍처표현 (Presentation), 응용 (Application), 도메인 (Domain), 인프라 (Infra)시스템 구성요소의 관심사 분리를 통해 유지보수와 확장을 용이하게 하는 구조
도메인 주도 설계전술적 설계 구성 요소Aggregate, Entity, Value Object도메인 복잡성 관리를 위한 핵심 구조화 방식 및 객체 정의
모델링 및 표현 도구UML클래스 다이어그램, 시퀀스 다이어그램 등도메인 구조와 상호작용을 시각적으로 표현하여 명확한 설계 커뮤니케이션 지원
개발 프로세스애자일 방법론반복적 개발, 지속적 통합, 고객 협업 중심도메인 전문가와 개발자의 지속 협업을 가능하게 하며, DDD 적용에 이상적 환경 제공

용어 정리

카테고리용어설명
DDD 핵심 개념Entity고유 식별자를 가지며 상태 변화와 생명주기를 관리하는 도메인 객체
Value Object불변성을 가지며 속성의 조합으로 동일성을 판단하는 객체
Aggregate연관된 도메인 객체 집합의 일관성을 보장하는 트랜잭션 단위
Aggregate RootAggregate 내부의 변경 진입점으로, 외부에서는 루트를 통해서만 Aggregate 접근 허용
Bounded Context하나의 도메인 모델이 명확하게 적용되는 경계, 모델 간 충돌을 방지함
설계 패턴Domain Event도메인 내에서 발생한 중요한 상태 변화를 외부에 알리는 이벤트 객체
Specification Pattern복잡한 비즈니스 규칙을 객체로 추출해 캡슐화하고 재사용성을 높이는 패턴
CQRSCommand(쓰기) 와 Query(읽기) 를 분리해 확장성과 성능을 향상하는 아키텍처 패턴
Event Sourcing시스템 상태를 이벤트의 이력으로 저장하고 재구성하는 아키텍처 패턴
보조 개념Guard도메인 객체 내부에서 즉시 유효성 검사를 수행해 불변식을 유지하는 방어 로직
Essence ObjectUI 입력 데이터의 일시적 보관과 검증을 담당하는 경량 전달 객체
설계 원칙Ubiquitous Language개발자와 도메인 전문가가 공유하는 공통 언어로, 코드와 문서에 동일하게 사용됨
SRP (단일 책임 원칙)클래스는 하나의 변경 이유만 가져야 하며 하나의 책임만 가져야 한다는 객체지향 원칙
Clean Architecture도메인 중심 아키텍처로, 비즈니스 로직과 인프라스트럭처를 계층 분리하여 의존성 방향을 고정함
아키텍처 도구Layered Architecture표현, 응용, 도메인, 인프라 계층으로 분리해 관심사와 책임을 명확히 하는 아키텍처
ArchUnitJava 코드의 아키텍처 규칙을 테스트 기반으로 자동 검증하는 도구
StructurizrC4 모델을 기반으로 시스템 아키텍처를 시각화하는 도구

참고 및 출처