전체 목록
설계Hard#120

DDD(Domain-Driven Design)의 Entity, Value Object, Aggregate를 설명해주세요.

#설계#DDD#Entity#ValueObject#Aggregate

답변 포인트

식별자로 구분되는 객체, 값으로 구분되는 객체, 일관성 경계를 구분해보세요.

정답 및 해설

빠른 요약

Entity는 식별자를 기준으로 구분되는 도메인 객체입니다. 속성이 바뀌어도 같은 ID를 가지면 같은 객체로 봅니다.

DDD는 복잡한 비즈니스 도메인을 코드에 잘 반영하기 위한 설계 접근입니다. 핵심은 기술 구조보다 도메인 모델과 비즈니스 규칙을 중심에 두는 것입니다.

Entity

Entity는 식별자를 기준으로 구분되는 객체입니다. 속성이 바뀌어도 같은 식별자를 가지면 같은 객체로 봅니다.

TypeScript
class User {
  constructor(
    public readonly id: string,
    public name: string,
    public email: string
  ) {}

  changeEmail(email: string) {
    this.email = email;
  }
}

사용자 이름이나 이메일이 바뀌어도 id가 같으면 같은 사용자입니다.

Value Object

Value Object는 식별자가 아니라 값 자체로 동등성을 판단하는 객체입니다. 보통 불변으로 설계합니다.

TypeScript
class Money {
  constructor(
    public readonly amount: number,
    public readonly currency: string
  ) {
    if (amount < 0) {
      throw new Error("금액은 음수일 수 없습니다.");
    }
  }

  add(other: Money) {
    if (this.currency !== other.currency) {
      throw new Error("통화가 다릅니다.");
    }

    return new Money(this.amount + other.amount, this.currency);
  }
}

Money(1000, "KRW") 두 개는 별도 식별자가 없어도 같은 값으로 볼 수 있습니다.

Entity와 Value Object 비교

구분EntityValue Object
동등성 기준식별자
변경 가능성상태 변경 가능불변 권장
User, Order, ProductMoney, Address, Email

Aggregate

Aggregate는 관련 Entity와 Value Object를 하나의 일관성 경계로 묶은 단위입니다. 외부에서는 Aggregate Root를 통해서만 내부 객체를 변경해야 합니다.

TypeScript
class Order {
  private items: OrderItem[] = [];

  constructor(public readonly id: string) {}

  addItem(productId: string, price: Money, quantity: number) {
    if (quantity <= 0) {
      throw new Error("수량은 1개 이상이어야 합니다.");
    }

    this.items.push(new OrderItem(productId, price, quantity));
  }

  getTotal() {
    return this.items.reduce(
      (total, item) => total.add(item.getSubtotal()),
      new Money(0, "KRW")
    );
  }
}

Order가 Aggregate Root라면 주문 항목 추가, 총액 계산, 주문 취소 같은 규칙은 Order를 통해 수행합니다.

Aggregate가 필요한 이유

  • 비즈니스 규칙을 한 곳에서 보호합니다.
  • 객체 간 불변 조건을 유지합니다.
  • 트랜잭션 경계를 명확히 합니다.
  • 외부 코드가 내부 상태를 마음대로 바꾸지 못하게 합니다.

예를 들어 "주문 총액은 주문 항목의 합과 같아야 한다"는 규칙은 Order Aggregate 내부에서 지켜야 합니다.

Repository

Repository는 Aggregate를 저장하고 조회하는 추상화입니다.

TypeScript
interface OrderRepository {
  findById(id: string): Promise<Order | null>;
  save(order: Order): Promise<void>;
}

도메인 로직은 DB가 MySQL인지 MongoDB인지 몰라도 됩니다.

정리

DDD에서 Entity는 식별자로 구분되는 도메인 객체, Value Object는 값으로 의미를 표현하는 불변 객체, Aggregate는 일관성을 지켜야 하는 객체 묶음입니다. 핵심은 비즈니스 규칙이 서비스 계층이나 컨트롤러에 흩어지지 않고 도메인 모델 안에 자연스럽게 자리 잡도록 설계하는 것입니다.

DDD 전술 패턴의 핵심

DDD에서 Entity, Value Object, Aggregate는 도메인 모델을 일관성 있게 표현하기 위한 기본 단위입니다.

Entity

식별자가 중요하고 시간이 지나며 속성이 변해도 같은 대상으로 취급됩니다.

TypeScript
class User {
  constructor(readonly id: UserId, private nickname: string) {}
  changeNickname(next: string) {
    if (next.length < 2) throw new Error('nickname too short');
    this.nickname = next;
  }
}

Value Object

식별자보다 값 자체가 중요하며 불변으로 다루는 것이 일반적입니다.

TypeScript
class Money {
  constructor(readonly amount: number, readonly currency: 'KRW' | 'USD') {
    if (amount < 0) throw new Error('negative money');
  }
}

Aggregate

관련 Entity/Value Object를 일관성 경계로 묶은 단위입니다. 외부에서는 Aggregate Root를 통해서만 내부를 변경하게 하여 규칙을 보호합니다.

TypeScript
class Order {
  private items: OrderItem[] = [];

  addItem(productId: string, quantity: number) {
    if (this.isPaid()) throw new Error('paid order cannot be changed');
    this.items.push(new OrderItem(productId, quantity));
  }
}

Aggregate는 DB 테이블 구조와 1:1로 맞추는 개념이 아닙니다. “어떤 변경을 하나의 트랜잭션으로 보호해야 하는가”를 기준으로 경계를 잡는 것이 중요합니다.

관련 질문

같은 카테고리/태그 기준