Files
2nd/10_Wiki/Topics/Domain_Programming/Coding/Arch_Domain_Events.md
T
Antigravity Agent c24165b8bc refactor(topics): 멀티 에이전트용 지식 재편 — _Common(공통 기본기) + Domain_* 구조
에이전트 8종(대화형/프로그래머 C·S/디자이너/설계자/기획자/QA/PD/PM)에게
[공통 기본 능력 + 롤별 Specialty] 2층으로 지식을 주입하기 위한 재분류.
문서 내용·포맷은 무수정, 폴더 이동만 (6,372개 문서 수 보존 확인).

- Topic_Programming → Domain_Programming (내부 구조 보존)
- Topic_Graphic → Domain_Design
- Topic_Business → Domain_Product
- Topic_General → Domain_General
- _Common 신설: Math(구 Topic_Math_Specialty), Reasoning(구 General/From_Thinking & Reasoning),
  Reasoning_Creativity(구 General/From_창의성), Communication(Poetic_Blog_Writing + From_writing)
- 타 도메인의 From_* 폴더는 유지 (출처 표기일 뿐, 이미 도메인에 맞게 분류된 문서)
- 빈 폴더 정리 (memory/procedures)
- 에이전트→폴더 매핑은 workspace의 .astra/agent-knowledge-map.json (9개 에이전트)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 11:05:56 +09:00

6.4 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-domain-events Domain Events — 발행 / 처리 / 형식 Coding draft B conceptual 2026-05-09 2026-05-09
architecture
ddd
events
vibe-coding
language applicable_to
TS
Backend
domain event
integration event
event handler
eventual consistency
in-process events

Domain Events

"이미 일어난 fact". Order Placed, Payment Received. Aggregate 안 발생 → Repository save 후 발행 → 다른 context / async handler 처리. In-process vs integration 분리.

📖 핵심 개념

  • Domain event: 도메인 안 사실. 과거형 동사 (Placed, Shipped).
  • Integration event: context 간 통신 — 보통 더 큰 payload + 안정 schema.
  • Synchronous vs Async.
  • Outbox 로 publish atomicity.

💻 코드 패턴

Event 정의

// 과거형 + 명사
abstract class DomainEvent {
  readonly id = uuid();
  readonly occurredAt = new Date();
  abstract readonly _tag: string;
}

class OrderPlaced extends DomainEvent {
  readonly _tag = 'OrderPlaced';
  constructor(public readonly orderId: OrderId, public readonly userId: UserId, public readonly total: Money) {
    super();
  }
}

class OrderShipped extends DomainEvent {
  readonly _tag = 'OrderShipped';
  constructor(public readonly orderId: OrderId, public readonly trackingNumber: string) {
    super();
  }
}

Aggregate 안 record

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

  static place(userId: UserId, items: OrderItem[]): Order {
    const order = new Order(OrderId.generate(), userId, items, 'placed');
    order.events.push(new OrderPlaced(order.id, userId, order.total()));
    return order;
  }

  ship(tracking: string) {
    if (this.status !== 'paid') throw new Error('cannot ship unpaid');
    this.status = 'shipped';
    this.events.push(new OrderShipped(this.id, tracking));
  }

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

In-process dispatch (synchronous)

class EventBus {
  private handlers = new Map<string, ((ev: DomainEvent) => void | Promise<void>)[]>();

  on<E extends DomainEvent>(tag: string, handler: (ev: E) => void | Promise<void>) {
    if (!this.handlers.has(tag)) this.handlers.set(tag, []);
    this.handlers.get(tag)!.push(handler as any);
  }

  async publish(events: DomainEvent[]) {
    for (const ev of events) {
      const hs = this.handlers.get(ev._tag) ?? [];
      for (const h of hs) await h(ev);
    }
  }
}

// register
bus.on<OrderPlaced>('OrderPlaced', async (ev) => {
  await sendOrderConfirmation(ev.userId, ev.orderId);
});

Repository 가 발행

class OrderRepository {
  constructor(private db: Db, private bus: EventBus) {}

  async save(order: Order) {
    const events = order.pullEvents();
    await this.db.transaction(async (tx) => {
      await tx.orders.upsert(toRow(order));
      // Outbox 패턴 — 같은 트랜잭션
      for (const ev of events) {
        await tx.outbox.insert({ type: ev._tag, payload: ev });
      }
    });
    // 트랜잭션 후에 in-process publish (best-effort)
    await this.bus.publish(events);
  }
}

Domain event vs Integration event

// Domain event — context 안, 작음
class OrderPlaced { orderId; userId; }

// Integration event — context 외, 풍부 + 안정 schema
class OrderPlacedIntegration {
  readonly version = '1.0';
  readonly _tag = 'order.placed';
  constructor(
    public readonly orderId: string,
    public readonly userId: string,
    public readonly customerEmail: string,    // shipping context 가 필요
    public readonly items: { productId: string; qty: number; price: number }[],
    public readonly total: { amount: number; currency: string },
    public readonly shippingAddress: { ... },
  ) {}
}

// Domain → Integration 변환
function toIntegration(ev: OrderPlaced, order: Order, user: User): OrderPlacedIntegration {
  return { ... };
}

→ Integration 가 다른 service 의 contract — 변경에 안정.

Async handler (background)

// outbox publisher 가 broker 로
on('OrderPlaced', async (ev: OrderPlacedIntegration) => {
  // 다른 service / context
  await emailService.send({ template: 'orderConfirmation', to: ev.customerEmail });
});

Sync vs Async 결정

Sync (in-process):
- 비즈니스 invariant 에 영향 (이메일 fail 시 order 도 rollback?)
- 같은 context 안

Async (background):
- 외부 통신 (이메일, 알림)
- 다른 context
- 실패 OK (재시도)

→ 보통 외부 효과 = async. 더 안전.

Event versioning

class OrderPlacedV2 {
  readonly _tag = 'order.placed.v2';
  // V1 + 새 필드
}

// Consumer 가 둘 다 처리
on('order.placed.v1', (ev) => handle(upgradeToV2(ev)));
on('order.placed.v2', (ev) => handle(ev));

→ Schema registry 같이 사용.

Event sourcing 과 차이

Domain event in CRUD:
- DB 가 source of truth (state 저장)
- Event 는 알림 / side effect

Domain event in ES:
- Event = source of truth
- State = events fold

이 문서는 CRUD + event 알림 패턴. ES 는 Backend_Event_Sourcing.

명명 convention

Past tense (이미 일어남):
✅ OrderPlaced, PaymentReceived, UserRegistered
❌ PlaceOrder (command — request, not fact)

Naming:
{Aggregate}{Action}
domain.{aggregate}.{action} — broker topic

🤔 의사결정 기준

사용 패턴
Aggregate 안 변경 알림 Domain event
Context 간 통신 Integration event + outbox
동기 검증 / 일관성 Sync handler
외부 효과 (email, push) Async + retry
Replay / audit Event sourcing
단순 CRUD event 없이 OK

안티패턴

  • Event 너무 잘게 (각 setter): noise. 비즈니스 의미만.
  • Event = command: 헷갈림. 동사 시제 검증.
  • Aggregate 직접 외부 publish: side-effect. Repository 가.
  • Sync handler 가 외부 호출: 트랜잭션 길어짐.
  • Schema breaking change: 새 version 만들기.
  • Event = current state: 그건 read model. event = 변경.
  • 모든 곳 listen — 없는 곳 없음: 결합 강 — 의도 명시.

🤖 LLM 활용 힌트

  • 과거형 + aggregate 별 명명.
  • Outbox + async 가 안전.
  • Domain ≠ Integration — 분리.

🔗 관련 문서