Files
2nd/10_Wiki/Topics/Coding/Arch_Aggregate_Design.md
T
2026-05-09 21:08:02 +09:00

6.7 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
arch-aggregate-design Aggregate Design — 일관성 / 경계 / Repository Coding draft B conceptual 2026-05-09 2026-05-09
architecture
ddd
aggregate
vibe-coding
language applicable_to
TS
Backend
aggregate
aggregate root
invariant
entity
value object
eventual consistency

Aggregate Design

Aggregate = 한 트랜잭션의 일관성 단위. 외부는 root 만 참조 (id). 작게 유지 — 큰 aggregate = lock contention. 다른 aggregate 간 = eventual consistency + event.

📖 핵심 개념

  • Entity: 정체성 (id) 있는 object.
  • Value Object: 값으로 비교 (Money, Address).
  • Aggregate Root: 외부 진입점. 다른 entity 보호.
  • Invariant: aggregate 안 항상 참인 규칙.

💻 코드 패턴

Aggregate root + 자식

// Order = aggregate root
export class Order {
  private items: OrderItem[] = [];
  private status: OrderStatus = 'open';

  constructor(public readonly id: OrderId, public readonly userId: UserId) {}

  // 외부는 method 만 — 직접 items push X
  addItem(productId: ProductId, qty: number, price: Money) {
    this.checkInvariants(() => {
      if (this.status !== 'open') throw new Error('order closed');
      if (this.items.length >= 100) throw new Error('too many items');
      if (qty <= 0) throw new Error('qty must be positive');
    });

    const existing = this.items.find(i => i.productId.equals(productId));
    if (existing) existing.increase(qty);
    else this.items.push(new OrderItem(productId, qty, price));
  }

  total(): Money {
    return this.items.reduce((s, i) => s.add(i.subtotal()), Money.zero('USD'));
  }

  ship() {
    if (this.items.length === 0) throw new Error('cannot ship empty');
    this.status = 'shipped';
  }

  private checkInvariants(extra?: () => void) {
    extra?.();
    if (this.total().amount.lt(0)) throw new Error('negative total');
  }
}

// OrderItem = entity 안 aggregate, 외부 못 봄
class OrderItem {
  constructor(public readonly productId: ProductId, private qty: number, private price: Money) {}
  increase(delta: number) { this.qty += delta; }
  subtotal() { return this.price.mul(this.qty); }
}

→ 외부는 Order.addItem(...) 만. order.items.push() 같은 직접 변경 금지.

Value Object

export class Money {
  constructor(public readonly amount: Decimal, public readonly currency: Currency) {
    if (amount.lt(0) && !this.allowNegative) throw new Error('negative');
  }

  add(other: Money): Money {
    if (this.currency !== other.currency) throw new Error('mismatch');
    return new Money(this.amount.add(other.amount), this.currency);
  }

  mul(n: number): Money { return new Money(this.amount.mul(n), this.currency); }
  
  equals(other: Money): boolean { return this.amount.eq(other.amount) && this.currency === other.currency; }
  
  static zero(c: Currency) { return new Money(new Decimal(0), c); }
}

// Immutable — `add` 가 새 Money 반환.

Aggregate 간 — id 만

// ❌ Order 가 User entity 직접 참조
class Order { user: User; }

// ✅ id 만
class Order { userId: UserId; }
// 필요 시 UserRepository.find(userId)

→ Memory + transaction 분리.

Repository (per aggregate)

interface OrderRepository {
  find(id: OrderId): Promise<Order | null>;
  save(order: Order): Promise<void>;
}

// 한 aggregate = 한 transaction
async function execute(input) {
  const order = await orderRepo.find(orderId);
  if (!order) throw ...;
  order.addItem(...);
  await orderRepo.save(order);
}

→ 다른 aggregate 같은 트랜잭션 안 변경 X.

Invariant 위반 체크

class Order {
  addItem(...) {
    // before
    const totalBefore = this.total();
    this.items.push(...);
    // after — invariant
    if (this.total().amount.gt(MAX_ORDER_LIMIT)) {
      this.items.pop();  // rollback
      throw new Error('order limit exceeded');
    }
  }
}

또는 immutable 스타일 — 새 instance 반환.

작게 유지 — split aggregate

// ❌ 큰 aggregate
class Order {
  items: OrderItem[];
  shipments: Shipment[];      // 다른 트랜잭션 변경
  invoices: Invoice[];        // 다른 트랜잭션 변경
}

// ✅
class Order { items: OrderItem[]; }
class Shipment { orderId: OrderId; ... }
class Invoice { orderId: OrderId; ... }

→ Order ↔ Shipment 동기화 = event + outbox + eventual consistency.

Domain event (aggregate 안 발생)

class Order {
  private events: DomainEvent[] = [];

  ship() {
    this.status = 'shipped';
    this.events.push(new OrderShipped(this.id, new Date()));
  }

  pullEvents(): DomainEvent[] {
    const e = this.events;
    this.events = [];
    return e;
  }
}

// repository.save 후 events publish
async function save(order: Order) {
  await db.transaction(async (tx) => {
    await tx.orders.upsert(toRow(order));
    for (const ev of order.pullEvents()) {
      await tx.outbox.insert({ type: ev._tag, payload: ev });
    }
  });
}

Concurrency (optimistic locking)

class Order {
  constructor(..., private version: number = 0) {}
  // 모든 변경 시 version++
}

// Repository
async save(order: Order) {
  const r = await db.update(orders)
    .set({ ...row, version: order.version + 1 })
    .where(and(eq(orders.id, order.id), eq(orders.version, order.version)));
  if (r.rowCount === 0) throw new ConcurrencyError();
}

→ 동시 변경 = retry.

Aggregate 크기 결정

좋은 경계:
- 한 트랜잭션 안에서 일관성 유지 가능
- 자주 같이 변경
- Invariant 가 묶여있음

나쁜 경계:
- 다른 사용자가 자주 변경 (lock 충돌)
- Items > 100
- Read 만 자주 (split read model)

🤔 의사결정 기준

패턴 사용
작은 aggregate (1-3 entity) 일반
Value object 모든 unit, money, time, address
큰 read Aggregate read X — projection
Cross-aggregate consistency event + saga
동시 변경 빈번 optimistic lock

안티패턴

  • God aggregate: User 가 모든 거 — lock 매번.
  • Aggregate 직접 참조 (memory): 다른 transaction 깨짐.
  • Setter 모든 필드: invariant 무의미. method 안.
  • Value Object mutable: 변경 시 race.
  • Repository CRUD generic: aggregate 의도 사라짐.
  • 모든 변경 = event: noise. 비즈니스 의미만.
  • Domain logic = service 만 (anemic): entity 는 dumb. 행위 entity 안.

🤖 LLM 활용 힌트

  • 작은 aggregate + id 참조 + event 통신.
  • Value Object 적극 (Money, Email, UserId).
  • Invariant 는 entity method 안.

🔗 관련 문서