DDD(도메인 주도 설계)
DDD(도메인 주도 설계)
원문: https://www.notion.so/1c9bf506e994804aabb2cd958ade1c2c?pvs=1
→ Domain Driven Design
[1] 서론
“도메인(=비즈니스의 핵심 개념과 규칙)을 중심에 두고 시스템을 설계하자” 는 철학이다
-
클린 아키텍처 : “기술을 분리해서 비즈니스 로직을 보호” 하는 구조
-
DDD : ”비즈니스 자체를 더 잘 이해하고, 코드에 녹여내기”라는 설계 방법
**1) 둘은 **이렇게 이어져 있다.
2) DDD 핵심 개념 미리보기
3) 그래서 왜 확장하는가?
4) 한 줄 요약
클린 아키텍처는 “어떻게 분리할까”를 알려주고 DDD는 “무엇을 설계할까”를 알려준다.
5) DDD 학습 순서
[2] DDD란 무엇인가?
(1) DDD 정의
“비즈니스 도메인을 코드에 그대로 반영하는 방식의 설계 철학”
즉, 개발자가 코드를 짤 때, “비즈니스 용어 그대로 코드에 녹이자”가 핵심이다.
(2) 왜 나왔을까?
-
시스템이 커질수록 기술적인 복잡함보다 비즈니스 로직이 더 복잡해짐
-
기존 방식은 기술 위주 구조 → 비즈니스 로직이 흩어지고 숨겨짐
-
그래서 도메인을 중심으로 설계하고 유지하자는 패러다임이 생긴 것이다.
(3) DDD의 특징
(4) 언제 DDD가 필요하나?
[3] 도메인, 도메인 모델이란?
(1) 도메인(Domain)이란?
”우리가 해결하려는 문제 영역” 즉, 비즈니스 자체를 의미한다.
예를 들어)
- 쇼핑몰 시스템이라면 : 상품, 장바구니, 주문, 결제, 배송
- 병원 시스템이라면 : 환자, 예약, 진료, 약 처방
클린 아키텍처의 핵심은 “비즈니스 로직 보호”였다.
→ DDD는 그 “비즈니스”를 도메인이라고 부른다.
(2) 도메인 모델(Domain Model)이란?
도메인을 코드로 표현한 것이 바로 도메인 모델이다.
즉, 도메인 모델은
- 비즈니스 규칙을 객체로 만든 것
- 예시) Order, Product, Payment, User 등
1) 도메인 모델의 핵심 특징
2) 예시로 이해하기 (쇼핑몰 도메인)
-
도메인 : “주문 처리”
-
도메인 모델 모습
3) 정리 요약
[4] Entity 와 Value Object 의 차이
(1) Entity란?
고유한 식별자(ID)를 가지는 객체
-
어떤 객체가 시스템 안에서 구분될 수 있어야 할 때 → Entity
-
보통 DB의 기본키(ID)와 1:1 대응됨
class User {
constructor(
public readonly id: string,
public name: string,
public email: string,
) {}
}
- 두 User 객체가 같은 name과 email을 가져도 id가 다르면 완전히 다른 사용자로 본다.
(2) Value Object란?
고유 ID는 없고, 값 자체는 중요한 객체
-
값이 같으면 동일한 객체로 취급한다.
-
불변으로 설계하는 경우가 많다.
class Address {
constructor(
public readonly city: string,
public readonly street: string,
) {}
equals(other: Address): boolean {
return this.city === other.city && this.street === other.street;
}
}
-
두 Address 객체의 city와 street가 같으면 → 같은 주소로 본다.
-
고유 ID가 없다.
(3) 핵심 차이 요약
1) 왜 구분이 중요할까?
-
Entity는 시스템에서 “이 사람”, “이 주문”처럼 개별 식별이 필요한 경우
-
Value Object는 그냥 값 일 뿐 → 복사, 비교, 불변으로 설계
⇒ 이 구분이 정확해야 애그리거트, 도메인 서비스 등도 올바르게 설계할 수 있다.
2) 코드로 표현
// order.entity.ts
export class Order {
constructor(
public readonly id: string, // Entity
public readonly address: Address, // Value Object
public readonly total: number,
) {}
}
// address.vo.ts
export class Address {
constructor(
public readonly city: string,
public readonly street: string,
) {}
}
-
Order는 Entity (id로 식별됨)
-
Address는 Value Object (값이 같으면 같은 객체로 취급)
3) 정리 요약
[5] Aggregate(애그리거트)란?
하나 이상의 엔티티 + 값 객체를 묶은 비즈니스 단위 단, 외부에서는 루트 엔티티(Aggregate Root)를 통해서만 접근 해야한다.
- 예시
(1) 예시 구조 (쇼핑몰)
```plain text [Order] ← Aggregate Root ├── OrderItem (Entity) └── ShippingAddress (Value Object)
→ Order는 전체 주문의 대표자
→ OrderItem을 직접 수정하지 않고, 항상 Order를 통해 관리해야 한다.
## (2) 왜 이렇게 나누는가?
## (3) 코드 예시
```typescript
// order.entity.ts
export class Order {
constructor(
public readonly id: string,
private items: OrderItem[], // 내부 Entity
private address: Address, // VO
) {}
addItem(productId: string, quantity: number) {
this.items.push(new OrderItem(productId, quantity));
}
getTotalPrice(): number {
return this.items.reduce((sum, item) => sum + item.getPrice(), 0);
}
}
// order-item.entity.ts
export class OrderItem {
constructor(
private readonly productId: string,
private quantity: number,
) {}
getPrice(): number {
// 예시: 1개당 10000원
return this.quantity * 10000;
}
}
→ OrderItem은 외부에서 직접 접근하지 않음
→ Order가 유일한 진입점 = Aggregate Root
(4) 한 줄 요약
Aggregate는 관련 객체들을 하나로 묶고, 그 중심 루트를 통해서만 접근하도록 만든 보호된 비즈니스 단위다.
[6] 도메인 서비스란?
도메인 로직인데, 특정 Entity에 넣기 애매할 때 사용하는 객체
즉,
-
비즈니스 로직은 맞는데
-
어떤 Entity에도 소속시키기 애매한 로직
-
또는 여러 Entity가 함께 관여되는 로직
→ 이걸 모아서 처리하는 것이 바로 “도메인 서비스”이다.
(1) 언제 필요한가?
(2) 예시 (포인트 전송 로직)
// PointTransferService.ts (도메인 서비스)
export class PointTransferService {
constructor(
private readonly accountRepo: AccountRepository,
) {}
transfer(senderId: string, receiverId: string, amount: number) {
const sender = this.accountRepo.findById(senderId);
const receiver = this.accountRepo.findById(receiverId);
sender.decreasePoint(amount);
receiver.increasePoint(amount);
this.accountRepo.save(sender);
this.accountRepo.save(receiver);
}
}
-
이 로직은 Account 하나 만으로는 해결할 수 없음
-
여러 Entity(sender, receiver)가 함께 관여됨
-
그래서 별도 서비스로 추출 = 도메인 서비스
(3) 도메인 서비스의 특징
(4) 도메인 서비스 vs 애플리케이션 서비스 차이
1) 예시로 설명
-
도메인 서비스
-
애플리케이션 서비스 (또는 인프라계층)
-
표로 정리
2) 그래서 이렇게 나눠야 한다.
(5) 한 줄 요약
도메인 서비스는 Entity에 넣기 애매한 “순수한 도메인 규칙”을 담는 stateless 객체다.
[7] Repository & Factory란?
(1) Repository란?
도메인 객체(Entity, Aggregate)를 저장하고 조회하는 인터페이스 즉, DB나 외부 저장소와의 연결을 도메인 계층 밖에서 담당하게 해주는 패턴이다.
1) 왜 필요한가?
-
도메인 객체는 DB를 직접 몰라야 한다.(→ DIP, 의존성 규칙)
-
하지만 저장/조회는 필요하니까 → 중간에 Repository를 둬서 분리
2) 예시
// domain/order.repository.ts
export interface OrderRepository {
save(order: Order): void;
findById(orderId: string): Order;
}
// infrastructure/mysql-order.repository.ts
@Injectable()
export class MySQLOrderRepository implements OrderRepository {
save(order: Order) {
// 실제 DB 저장 로직 (TypeORM, Prisma 등)
}
findById(orderId: string) {
// 실제 조회 로직
}
}
→ 도메인 서비스나 UseCase는 OrderRepository 인터페이스만 알고 있다.
→ 구현체는 바깥(infrastructure 계층)에서 주입
(2) Factory란?
복잡한 도메인 객체 생성을 담당하는 객체 단순한 new Order(…)로 만들기 힘들 때 사용한다.
1) 언제 필요한가?
-
Entity 생성 시 내부 규칙이 많을 때 (예: 할인, 상태 조건, 여러 값 조합 등)
-
외부에서 “생성 로직”을 통일하고 싶은 경우
2) 예시
// domain/order.factory.ts
export class OrderFactory {
static createFromCart(cart: Cart): Order {
const items = cart.getItems();
const total = items.reduce((sum, item) => sum + item.getPrice(), 0);
return new Order('generated-id', items, total);
}
}
→ Order 생성에 필요한 조건, 계산 등을 Factory가 알아서 처리
(3) 둘의 역할 비교
Repository는 “어디서 가져오고, 어떻게 저장할까”를 담당 Factory는 “어떻게 만들까?”를 담당
[8] 유비쿼터스 언어란?
개발자, 기획자, 비즈니스 담당자가 모두 공유하는 공통언어를 뜻한다.
즉,
-
도메인 전문가(비즈니스 담당자)와
-
개발자가
→ 같은 말로 대화하고, 같은 용어로 모델을 설계하자는 개념이다.
(1) 왜 필요한가?
(2) 유비쿼터스 언어를 쓰면
-
코드 = 대화 = 문서 = 모델 → 전부 같은 말
-
기획서에 있는 용어가 그대로 클래스/메서드/변수 이름이 됨
-
비즈니스 용어가 코드에 녹아 들어감
(3) 예시
1) 나쁜 코드 (기술 중심)
if (user.role === 'admin') {
// ...
}
→ 나중에 “운영자”, “슈퍼관리자”도 추가되면 의미가 불명확해짐
2) 좋은 코드 (도메인 언어 중심)
if (user.canApproveOrders()) {
// 주문 승인 로직
}
-
canApproveOrders()는 비즈니스 용어
-
“누가 주문을 승인할 수 있나?”라는 질문에 그대로 대응 가능
-
유지보수, 소통, 테스트 모두 쉬움
(4) 실제 프로젝트 적용법
(5) 요약
유비쿼터스 언어는 “모두가 같은 용어로 시스템을 이해하고 설계하게 해주는 DDD의 핵심 소통 도구이다.
[9] NestJS에 DDD + 클린 아키텍처를 적용한 예시
(1) 전체 구조 예시
src/
├── domain/
│ └── order/
│ ├── order.entity.ts # Entity
│ ├── order-item.entity.ts # Entity
│ ├── address.vo.ts # Value Object
│ ├── order.repository.ts # Repository Interface
│ └── order.factory.ts # Factory
│
├── application/
│ └── use-cases/
│ └── create-order.use-case.ts # Application Service (UseCase)
│
├── infrastructure/
│ └── repositories/
│ └── mysql-order.repository.ts # Repository 구현체 (DB 기술)
│
├── interfaces/
│ └── controllers/
│ └── order.controller.ts # HTTP 요청 처리
│ └── dtos/
│ └── create-order.dto.ts # 요청/응답 DTO
│
├── config/
│ └── app.module.ts # 의존성 주입 설정
(2) 주문 생성하기
1) [인터페이스 계층] HTTP 요청
// interfaces/controllers/order.controller.ts
@Post()
createOrder(@Body() dto: CreateOrderDto) {
return this.createOrderUseCase.execute(dto);
}
2) [애플리케이션 계층] UseCase 실행
// application/use-cases/create-order.use-case.ts
export class CreateOrderUseCase {
constructor(
private readonly repo: OrderRepository,
private readonly factory: OrderFactory,
) {}
execute(dto: CreateOrderDto) {
const order = this.factory.createFromDto(dto); // 생성 책임은 Factory
this.repo.save(order); // 저장은 Repository
return order;
}
}
3) [도메인 계층] 도메인 모델 정의
// domain/order/order.entity.ts
export class Order {
constructor(
private readonly id: string,
private readonly items: OrderItem[],
private readonly address: Address,
) {}
getTotalAmount(): number {
return this.items.reduce((sum, item) => sum + item.getPrice(), 0);
}
}
4) [인프라 계층] 실제 저장소 구현
// infrastructure/repositories/mysql-order.repository.ts
@Injectable()
export class MySQLOrderRepository implements OrderRepository {
save(order: Order) {
// TypeORM, Prisma 등으로 DB 저장
}
findById(id: string): Order {
// 조회 로직
}
}
5) [config] DI 설정
// config/app.module.ts
@Module({
controllers: [OrderController],
providers: [
CreateOrderUseCase,
OrderFactory,
{
provide: OrderRepository,
useClass: MySQLOrderRepository,
},
],
})
export class AppModule {}
(3) 각 기술이 어디에 위치하는 지 요약
(4) 장점
(5) 클린 아키텍처 vs DDD
1) 실무에선 어떻게 되냐면
-
클린 아키텍처 구조 위에 DDD를 얹는다.
-
즉, 계층 구조는 클린 아키텍처 따라가고,
-
그 안에 들어가는 도메인 모델은 DDD 철학을 따른다.
plain text
[ Order (Entity) ] ← DDD
[ OrderService (UseCase)] ← 클린 아키텍처
[OrderController / Repo ] ← 클린 아키텍처
(6) 요약
클린 아키텍처 + DDD 조합은 복잡한 시스템을 “비즈니스 중심 + 기술 유연성” 구조로 설계할 수 있게 해준다.
즉,
클린 아키텍처는 “틀”이고, DDD는 그 틀 안에 채워 넣는 “내용과 언어”다.
Comments