도메인 주도 설계 — 패턴 및 모범 사례은(는) 무엇을 하나요?
DDD를 사용해야 하는 경우
사용해야 하는 경우:
- CRUD를 넘어서는 복잡한 비즈니스 로직
- 서로 다른 용어를 사용하는 여러 도메인 전문가
- 도메인 모델이 경쟁 우위인 경우
- 엔터프라이즈급 애플리케이션
사용하지 말아야 하는 경우:
- 단순 CRUD 애플리케이션
- 해커톤/MVP (과잉)
- 도메인 전문가가 없는 경우
전략적 설계
Bounded Context(제한된 컨텍스트)
DDD에서 가장 중요한 개념입니다. Bounded Context는 특정 모델이 정의되고 적용되는 경계입니다.
예시 — 전자상거래:
[주문 컨텍스트] [결제 컨텍스트] [배송 컨텍스트]
- 주문 - 결제 - 배송
- 주문항목 - 거래 - 추적번호
- 고객 (참조) - 환불 - 주소
- 주소 (값 객체) - 청구서 - 운송사
고객은 각 컨텍스트에서 다른 의미를 가집니다:
- 주문 컨텍스트: 이름, 이메일, 배송 선호도
- 결제 컨텍스트: 청구 정보, 결제 방법
- 지원 컨텍스트: 티켓 내역, 만족도 점수
컨텍스트 맵
[주문] ←→ [결제] # 파트너십
[주문] → [배송] # 고객-공급자
[주문] → [레거시 CRM] # 반부식 계층(Anti-Corruption Layer)
전술적 설계
엔티티(Entities)
식별자를 가진 객체입니다. 동일한 속성을 가져도 ID가 다르면 다른 엔티티입니다.
class User {
readonly id: UserId;
name: string;
email: Email; // 값 객체
}
값 객체(Value Objects)
속성에 의해 정의되며 식별자가 없는 객체입니다. 불변(immutable)입니다.
class Email {
constructor(readonly value: string) {
if (!isValidEmail(value)) throw new InvalidEmailError(value);
}
equals(other: Email): boolean {
return this.value === other.value;
}
}
애그리게이트(Aggregates)
단일 루트 엔티티(Aggregate Root)를 가진 엔티티와 값 객체의 클러스터입니다. 모든 접근은 루트를 통해 이루어집니다.
class Order { // Aggregate Root
private items: OrderItem[] = [];
addItem(product: ProductRef, quantity: number): void {
// 비즈니스 로직은 서비스가 아닌 여기에 작성
if (quantity <= 0) throw new InvalidQuantityError();
this.items.push(new OrderItem(product, quantity));
}
get total(): Money {
return this.items.reduce((sum, item) => sum.add(item.subtotal), Money.zero());
}
}
애그리게이트 규칙:
- 다른 애그리게이트는 ID로만 참조
- 트랜잭션당 하나의 애그리게이트
- 애그리게이트 간 최종 일관성(Eventual Consistency)
도메인 이벤트(Domain Events)
도메인 전문가가 관심을 가지는 도메인 내에서 발생한 사건입니다.
class OrderPlaced implements DomainEvent {
constructor(
readonly orderId: OrderId,
readonly customerId: CustomerId,
readonly total: Money,
readonly occurredAt: Date
) {}
}
도메인 서비스(Domain Services)
엔티티나 값 객체에 자연스럽게 속하지 않는 비즈니스 로직입니다.
class PricingService {
calculatePrice(order: Order, customer: Customer, promotions: Promotion[]): Money {
// 여러 애그리게이트를 포함하는 복잡한 가격 계산 로직
}
}
리포지토리(Repositories)
데이터 접근에 대한 추상화입니다. 애그리게이트 루트당 하나의 리포지토리입니다.
interface OrderRepository {
findById(id: OrderId): Promise<Order | null>;
save(order: Order): Promise<void>;
delete(id: OrderId): Promise<void>;
}
디렉터리 구조 (DDD)
src/
├── modules/
│ └── orders/ # Bounded Context
│ ├── domain/
│ │ ├── entities/
│ │ │ └── order.ts # Aggregate Root
│ │ ├── value-objects/
│ │ │ └── money.ts
│ │ ├── events/
│ │ │ └── order-placed.ts
│ │ ├── services/
│ │ │ └── pricing.ts
│ │ └── repositories/
│ │ └── order.repository.ts # 인터페이스
│ ├── application/
│ │ ├── commands/
│ │ │ └── place-order.ts
│ │ ├── queries/
│ │ │ └── get-order.ts
│ │ └── handlers/
│ │ └── place-order.handler.ts
│ └── infrastructure/
│ ├── persistence/
│ │ └── order.repository.impl.ts # 구현체
│ └── api/
│ └── orders.controller.ts
클린 아키텍처 의존성 규칙
DDD에서 의존성은 반드시 안쪽으로 향해야 합니다:
[ 프레임워크 & 드라이버 (Web, DB, UI) ]
└──▶ [ 인터페이스 어댑터 (컨트롤러, 게이트웨이) ]
└──▶ [ 애플리케이션 (유스 케이스, CQRS 핸들러) ]
└──▶ [ 도메인 (엔티티, 값 객체) ]
- 도메인 계층은 ORM(Prisma, TypeORM), HTTP 프레임워크(Express, NestJS), 외부 SDK에 대해 전혀 의존성이 없어야 합니다.
- 리포지토리는 도메인/애플리케이션 계층에서 인터페이스로 정의되고, 인프라 계층에서 구현됩니다.
도메인 이벤트 vs 통합 이벤트(Integration Events)
- 도메인 이벤트: 단일 Bounded Context 내부의 상태 변경을 나타냅니다.
- Aggregate Root 내에서 직접 발생시킵니다 (
order.addItem(...)가OrderItemAdded발생). - 트랜잭션 커밋 전에 인-프로세스로 전달됩니다.
- Aggregate Root 내에서 직접 발생시킵니다 (
- 통합 이벤트: Bounded Context 경계를 넘어 다른 서비스와 통신하기 위해 발행됩니다.
- 트랜잭셔널 아웃박스(Transactional Outbox) 패턴을 통해 메시지 브로커로 전달됩니다.
- 버전 관리를 포함한 하위 호환 가능한 스키마를 사용해야 합니다.
반부식 계층(Anti-Corruption Layer, ACL)
외부 Bounded Context나 타사 벤더 API(예: Stripe, Salesforce)의 데이터를 소비할 때:
- 외부 도메인 모델을 절대 도메인으로 직접 가져오지 마세요.
- 인프라 계층에 ACL 변환기/어댑터를 만들어 외부 DTO를 자체 값 객체와 엔티티로 변환하세요.
안티 패턴
- [실패] 빈약한 도메인 모델(Anemic domain model) — getter/setter만 있는 엔티티, 모든 로직이 서비스에 있음
- [실패] 거대 애그리게이트(Big aggregate) — 애그리게이트는 작고 불변성에 집중해야 함
- [실패] 애그리게이트 간 트랜잭션 — 최종 일관성 사용
- [실패] DDD를 모든 곳에 적용 — 복잡성이 정당화되는 곳에만 DDD 사용
- [실패] ORM 엔티티가 도메인으로 유출 — 도메인 엔티티는
@Entity()또는 ORM 데코레이터에 의존해서는 안 됨
코드 예제
자세한 코드 예제는 EXAMPLES.md를 참조하세요.
검증 체크리스트
작업 완료 전 검토 단계에서 확인할 사항입니다.
일반적인 실수
명시적으로 피해야 할 안티 패턴 및 사항입니다. TROUBLESHOOTING.md를 참조하세요.
통합 노트
이 스킬이 다른 스킬과 어떻게 상호작용하는지 설명합니다.