본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 10. 31. · 23 Views
DDD 도메인 주도 설계 핵심 개념
도메인 주도 설계(DDD)의 핵심 개념과 실전 패턴을 TypeScript 코드로 배웁니다. Entity, Value Object, Aggregate, Repository 등 DDD의 주요 빌딩 블록을 실무에 바로 적용할 수 있도록 구체적인 예제와 함께 설명합니다.
들어가며
이 글에서는 DDD 도메인 주도 설계 핵심 개념에 대해 상세히 알아보겠습니다. 총 10가지 주요 개념을 다루며, 각각의 개념에 대한 설명과 실제 코드 예제를 함께 제공합니다.
목차
- Entity_기본_구조
- Value_Object_불변성
- Aggregate_일관성_경계
- Repository_인터페이스
- Domain_Service
- Factory_패턴
- Domain_Event
- Specification_패턴
- Bounded_Context_분리
- Anti_Corruption_Layer
1. Entity 기본 구조
개요
Entity는 고유한 식별자(ID)를 가지며 생명주기 동안 속성이 변경될 수 있는 객체입니다. ID가 같으면 동일한 Entity로 판단합니다.
코드 예제
class User {
constructor(
private readonly id: string,
private name: string,
private email: string
) {}
changeName(newName: string): void {
this.name = newName;
}
equals(other: User): boolean {
return this.id === other.id;
}
}
설명
User Entity는 고유 ID로 식별되며, changeName으로 상태를 변경할 수 있습니다. equals 메서드는 ID를 비교하여 동일성을 판단합니다.
2. Value Object 불변성
개요
Value Object는 식별자가 없고 속성 값으로만 비교되는 불변 객체입니다. 같은 속성을 가지면 동일한 객체로 취급합니다.
코드 예제
class Money {
constructor(
private readonly amount: number,
private readonly currency: string
) {}
add(other: Money): Money {
if (this.currency !== other.currency) {
throw new Error('통화가 다릅니다');
}
return new Money(this.amount + other.amount, this.currency);
}
equals(other: Money): boolean {
return this.amount === other.amount && this.currency === other.currency;
}
}
설명
Money는 불변 객체로, add 연산 시 새로운 인스턴스를 반환합니다. 금액과 통화가 같으면 동일한 Value Object입니다.
3. Aggregate 일관성 경계
개요
Aggregate는 관련된 객체들을 하나의 단위로 묶어 일관성을 보장합니다. Aggregate Root를 통해서만 내부 객체에 접근할 수 있습니다.
코드 예제
class Order {
private items: OrderItem[] = [];
constructor(private readonly id: string) {}
addItem(product: string, quantity: number, price: Money): void {
const item = new OrderItem(product, quantity, price);
this.items.push(item);
}
getTotalAmount(): Money {
return this.items.reduce(
(total, item) => total.add(item.getAmount()),
new Money(0, 'KRW')
);
}
}
설명
Order는 Aggregate Root로서 OrderItem들을 관리합니다. 외부에서는 Order를 통해서만 주문 항목을 추가하고 총액을 계산할 수 있습니다.
4. Repository 인터페이스
개요
Repository는 도메인 객체의 영속성을 추상화하는 패턴입니다. 도메인 계층에서는 인터페이스만 정의하고 구현은 인프라 계층에서 합니다.
코드 예제
interface UserRepository {
findById(id: string): Promise<User | null>;
save(user: User): Promise<void>;
delete(id: string): Promise<void>;
}
class UserService {
constructor(private userRepo: UserRepository) {}
async changeUserName(id: string, newName: string): Promise<void> {
const user = await this.userRepo.findById(id);
if (user) {
user.changeName(newName);
await this.userRepo.save(user);
}
}
}
설명
UserRepository 인터페이스로 저장소를 추상화하여 도메인 로직이 데이터베이스 구현에 의존하지 않도록 합니다.
5. Domain Service
개요
Domain Service는 특정 Entity나 Value Object에 속하지 않는 도메인 로직을 캡슐화합니다. 여러 도메인 객체를 조율하는 역할을 합니다.
코드 예제
class TransferService {
transfer(from: Account, to: Account, amount: Money): void {
if (!from.canWithdraw(amount)) {
throw new Error('잔액이 부족합니다');
}
from.withdraw(amount);
to.deposit(amount);
}
}
class Account {
constructor(private balance: Money) {}
canWithdraw(amount: Money): boolean {
return this.balance.amount >= amount.amount;
}
withdraw(amount: Money): void { /* ... */ }
deposit(amount: Money): void { /* ... */ }
}
설명
계좌 이체는 두 개의 Account를 조율해야 하므로 Domain Service로 구현합니다. 단일 Entity의 책임을 넘어서는 로직입니다.
6. Factory 패턴
개요
Factory는 복잡한 도메인 객체 생성 로직을 캡슐화합니다. 생성 규칙이 복잡하거나 여러 객체를 조합해야 할 때 사용합니다.
코드 예제
class OrderFactory {
createOrder(customerId: string, items: Array<{
productId: string;
quantity: number;
price: number;
}>): Order {
const order = new Order(this.generateOrderId(), customerId);
items.forEach(item => {
order.addItem(
item.productId,
item.quantity,
new Money(item.price, 'KRW')
);
});
return order;
}
private generateOrderId(): string {
return `ORD-${Date.now()}`;
}
}
설명
OrderFactory는 주문 생성의 복잡한 로직을 캡슐화하여 Order 객체 생성을 단순화합니다.
7. Domain Event
개요
Domain Event는 도메인에서 발생한 중요한 사건을 나타냅니다. 다른 Aggregate나 외부 시스템에 변경 사항을 알릴 때 사용합니다.
코드 예제
interface DomainEvent {
occurredOn: Date;
}
class OrderPlaced implements DomainEvent {
constructor(
public readonly orderId: string,
public readonly customerId: string,
public readonly occurredOn: Date = new Date()
) {}
}
class Order {
private events: DomainEvent[] = [];
place(): void {
// 주문 처리 로직
this.events.push(new OrderPlaced(this.id, this.customerId));
}
getEvents(): DomainEvent[] {
return [...this.events];
}
}
설명
Order가 접수되면 OrderPlaced 이벤트를 발행하여 재고 차감, 알림 발송 등 후속 처리를 트리거할 수 있습니다.
8. Specification 패턴
개요
Specification은 비즈니스 규칙을 재사용 가능한 객체로 캡슐화합니다. 복잡한 조건 로직을 조합하고 재사용할 수 있습니다.
코드 예제
interface Specification<T> {
isSatisfiedBy(candidate: T): boolean;
}
class PremiumCustomerSpec implements Specification<Customer> {
isSatisfiedBy(customer: Customer): boolean {
return customer.totalPurchase >= 1000000;
}
}
class LoyalCustomerSpec implements Specification<Customer> {
isSatisfiedBy(customer: Customer): boolean {
return customer.membershipYears >= 3;
}
}
// 사용 예
const spec = new PremiumCustomerSpec();
if (spec.isSatisfiedBy(customer)) {
applyDiscount(customer);
}
설명
비즈니스 규칙을 Specification으로 분리하여 테스트와 재사용이 쉬워지고, 규칙 조합도 가능합니다.
9. Bounded Context 분리
개요
Bounded Context는 특정 도메인 모델이 적용되는 경계입니다. 같은 용어도 컨텍스트에 따라 다른 의미를 가질 수 있습니다.
코드 예제
// 주문 컨텍스트
namespace OrderContext {
export class Product {
constructor(
public id: string,
public name: string,
public price: Money
) {}
}
}
// 재고 컨텍스트
namespace InventoryContext {
export class Product {
constructor(
public id: string,
public stockQuantity: number,
public location: string
) {}
}
}
설명
Product는 컨텍스트마다 다른 속성과 책임을 가집니다. 주문에서는 가격이 중요하지만 재고에서는 수량과 위치가 중요합니다.
10. Anti Corruption Layer
개요
Anti-Corruption Layer는 외부 시스템과의 통합 시 도메인 모델을 보호하는 번역 계층입니다. 외부 모델이 내부 모델을 오염시키지 않도록 합니다.
코드 예제
// 외부 결제 시스템 응답
interface ExternalPaymentResponse {
payment_id: string;
status_code: number;
amount_cents: number;
}
// Anti-Corruption Layer
class PaymentAdapter {
toDomain(response: ExternalPaymentResponse): PaymentResult {
return new PaymentResult(
response.payment_id,
this.mapStatus(response.status_code),
new Money(response.amount_cents / 100, 'KRW')
);
}
private mapStatus(code: number): PaymentStatus {
return code === 200 ? 'SUCCESS' : 'FAILED';
}
}
설명
PaymentAdapter가 외부 API 응답을 도메인 모델로 변환하여 외부 시스템의 변경이 도메인에 영향을 주지 않도록 보호합니다.
마치며
이번 글에서는 DDD 도메인 주도 설계 핵심 개념에 대해 알아보았습니다. 총 10가지 개념을 다루었으며, 각각의 사용법과 예제를 살펴보았습니다.
관련 태그
#TypeScript #DDD #Entity #ValueObject #Aggregate
댓글 (0)
함께 보면 좋은 카드 뉴스
마이크로서비스 배포 완벽 가이드
Kubernetes를 활용한 마이크로서비스 배포의 핵심 개념부터 실전 운영까지, 초급 개발자도 쉽게 따라할 수 있는 완벽 가이드입니다. 실무에서 바로 적용 가능한 배포 전략과 노하우를 담았습니다.
데이터 영속성 JPA 완벽 가이드
자바 개발자라면 반드시 알아야 할 JPA의 핵심 개념부터 실무 활용법까지 담았습니다. 엔티티 설계부터 연관관계 매핑까지, 초급 개발자도 쉽게 이해할 수 있도록 친절하게 설명합니다.
Application Load Balancer 완벽 가이드
AWS의 Application Load Balancer를 처음 배우는 개발자를 위한 실전 가이드입니다. ALB 생성부터 ECS 연동, 헬스 체크, HTTPS 설정까지 실무에 필요한 모든 내용을 다룹니다. 초급 개발자도 쉽게 따라할 수 있도록 단계별로 설명합니다.
고객 상담 AI 시스템 완벽 구축 가이드
AWS Bedrock Agent와 Knowledge Base를 활용하여 실시간 고객 상담 AI 시스템을 구축하는 방법을 단계별로 학습합니다. RAG 기반 지식 검색부터 Guardrails 안전 장치, 프론트엔드 연동까지 실무에 바로 적용 가능한 완전한 시스템을 만들어봅니다.
에러 처리와 폴백 완벽 가이드
AWS API 호출 시 발생하는 에러를 처리하고 폴백 전략을 구현하는 방법을 다룹니다. ThrottlingException부터 서킷 브레이커 패턴까지, 실전에서 바로 활용할 수 있는 안정적인 에러 처리 기법을 배웁니다.