DDD (Domain-Driven Design)
도메인 주도 설계란 복잡한 비즈니스 로직을 기술 중심이 아니라 도메인(업무 영역) 중심으로 설계하는 접근법입니다.
핵심 철학
단순한 설계 패턴이 아니라, 시스템을 바라보는 관점 자체를 바꾸는 것 “단순히 코드 구조를 깔끔히 나누자”가 아닌 “비즈니스 언어를 코드로 표현하자”
- 도메인 중심 사고 : 데이터 구조가 아닌, 비즈니스 규칙과 개념을 중심으로 모델링
- 언어의 일관성 (유비쿼터스 언어) : 도메인 전문가와 개발자가 같은 용어로 대화
- 경계 설정 (Bounded Context) : 도메인을 여러 하위 영역으로 분리해 복잡도 관리
- 모델과 코드의 일치 : 코드 구조가 실제 도메인 구조를 그대로 반영
- 변화에 강한 구조 : 기술 변경(DB, ORM 등)에도 도메인 로직이 영향을 받지 않음
즉, DDD의 핵심은 “코드와 비즈니스 언어의 일치”입니다.
핵심 용어
도메인(Domain)
영화 예매 시스템 : 예매, 결제, 회원, 상영
엔티티(Entity)
- 고유한 식별자(ID)를 가지는 객체
- 속성 값이 바뀌어도 동일한 객체로 간주
- 비즈니스 규칙이 직접 담기며, 상태 변화가 허용
1
2
3
4
5
6
| class Booking {
constructor(public id: string, public userId: string, public seat: string) {}
cancel() {
// 예매 취소 규칙
}
}
|
값 객체(Value Object)
- 식별자가 없고, 값으로만 구분되는 객체
- 불변(Immutable)이며, 의미 단위를 표현
- 여러 엔티티에서 공유 가능한 개념
1
2
3
4
5
6
7
8
| class Money {
constructor(private amount: number, private currency: string) {}
add(value: Money): Money {
if (this.currency !== value.currency) throw new Error("통화 단위 불일치");
return new Money(this.amount + value.amount, this.currency);
}
}
|
Aggregate (집합체)
- 여러 Entity와 Value Object를 하나로 묶은 단위
- 비즈니스 규칙상 함께 변경되어야 하는 객체들의 집합
- 불변성의 단위를 보장하는 루트 엔티티(Aggregate Root)를 갖음
1
2
3
4
| BookingAggregate
├── Booking (Root Entity)
├── Payment (Entity)
└── Seat (Value Object)
|
Repository (저장소)
- Aggregate 단위로 데이터를 영속화하고 조회하는 역할
- 기술적인 DB 접근 로직(Infrastructure)을 도메인에서 분리
1
2
3
4
| interface BookingRepository {
findById(id: string): Promise<Booking>;
save(booking: Booking): Promise<void>;
}
|
Domain Service
- 여러 Entity나 VO가 협력해야 하는 도메인 로직을 담당
- 특정 엔티티 하나에 속하지 않는 순수 비즈니스 규칙
1
2
3
4
5
6
7
8
| class BookingService {
constructor(private readonly paymentService: PaymentService) {}
async reserve(booking: Booking, seat: Seat) {
if (!seat.isAvailable()) throw new Error("좌석이 이미 예약되었습니다.");
await this.paymentService.pay(booking);
}
}
|
Application Service
- 사용자의 유스케이스(Use Case)를 수행하는 계층
- 트랜잭션 단위로 동작하며, 여러 도메인 서비스를 조합
- 비즈니스 로직은 없고, 흐름 제어 집중
1
2
3
4
5
6
7
8
9
10
11
12
| class BookingApplicationService {
constructor(
private readonly bookingRepo: BookingRepository,
private readonly bookingService: BookingService
) {}
async reserve(userId: string, seatId: string) {
const booking = new Booking(userId, seatId);
await this.bookingService.reserve(booking, seatId);
await this.bookingRepo.save(booking);
}
}
|
Infrastructure
- DB, 외부 API, 메일 서비스 등 기술적 세부 구현을 담당
- 도메인 계층에 의존하지 않도록 인터페이스 기반으로 추상화
1
2
3
4
5
6
| class PrismaBookingRepository implements BookingRepository {
async findById(id: string): Promise<Booking> {
const data = await prisma.booking.findUnique({ where: { id } });
return new Booking(data.id, data.userId, data.seat);
}
}
|
유비쿼터스 언어 (Ubiquitous Language)
- DDD에서 가장 중요한 원칙 중 하나
- “도메인 전문가와 개발자가 같은 언어로 이야기해야 한다.”
| 비즈니스 용어 | 코드 표현 |
|---|
| 예매 취소 | cancelBooking() |
| 좌석 점유 | reserveSeat() |
| 결제 승인 | approvePayment() |
언어의 일관성이 유지될수록 시스템을 이해하기 쉬워집니다. 이는 단순한 네이밍 규칙이 아니라, 커뮤니케이션과 설계의 일관성을 유지하기 위한 핵심 원칙입니다.
장점
- 복잡한 비즈니스 로직의 명확화 : 규칙과 개념이 코드로 드러남
- 유지보수성 향상 : 각 계층이 독립적이며 수정 영향이 작음
- 테스트 용이성 : 도메인 로직만 단독으로 테스트 가능
- 변화에 유연함 : DB, ORM, 프레임워크 변경에도 도메인 불변
- 팀 커뮤니케이션 향상 : 도메인 언어가 통일되어 협업 효율 증가
계층 구조 (Layered Architecture)
DDD는 보통 4계층 구조로 표현됩니다.
| 계층 | 역할 | 예시 |
|---|
| Presentation Layer | 사용자 요청/응답 처리 (HTTP, GraphQL 등) | Controller |
| Application Layer | 유스케이스 조합 및 실행 | Application Service |
| Domain Layer | 핵심 비즈니스 로직, 규칙 | Entity, Value Object, Domain Service |
| Infrastructure Layer | DB, 외부 API, 기술 의존 구현 | Repository, Adapter |
MVC와의 차이
| 비교 항목 | 전통적인 3계층 (MVC) | DDD (4계층 구조) |
|---|
| 중심 사고 | 기술 중심 | 비즈니스 중심 |
| 모델 | 단순 데이터 구조 | 풍부한 도메인 모델 |
| 로직 위치 | Controller / Service 혼재 | Domain / Application으로 명확히 분리 |
| 확장성 | 비즈니스 커지면 복잡도 급증 | 계층별로 분리되어 유지보수 용이 |
| 테스트 | 통합 테스트 중심 | 단위 테스트 가능 |