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>
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
---
|
||||
id: backend-event-sourcing
|
||||
title: Event Sourcing — 이벤트 기반 상태
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [backend, event-sourcing, ddd, vibe-coding]
|
||||
tech_stack: { language: "TS / SQL / EventStore", applicable_to: ["Backend"] }
|
||||
applied_in: []
|
||||
aliases: [event sourcing, event store, snapshot, projection, replay]
|
||||
---
|
||||
|
||||
# Event Sourcing
|
||||
|
||||
> 상태 = 모든 이벤트의 fold. **현재 상태 저장 X — 이벤트 append-only 저장**. 재구성 / time-travel / audit 자연. 단 학습 곡선 + 복잡 — 모든 도메인에 안 어울림.
|
||||
|
||||
## 📖 핵심 개념
|
||||
- Event: 과거 fact (`OrderCreated`, `ItemAdded`).
|
||||
- Aggregate: 일관성 경계 + 이벤트 producer.
|
||||
- Stream: aggregate 의 이벤트 list.
|
||||
- Projection: 이벤트 → read model 만들기.
|
||||
- Snapshot: 빠른 복원 위해 N 이벤트마다.
|
||||
|
||||
## 💻 코드 패턴
|
||||
|
||||
### Event 정의
|
||||
```ts
|
||||
type OrderEvent =
|
||||
| { type: 'OrderCreated'; data: { orderId: string; userId: string; createdAt: string } }
|
||||
| { type: 'ItemAdded'; data: { orderId: string; itemId: string; qty: number } }
|
||||
| { type: 'OrderShipped'; data: { orderId: string; shippedAt: string } }
|
||||
| { type: 'OrderCancelled'; data: { orderId: string; reason: string } };
|
||||
```
|
||||
|
||||
### Aggregate (state from events)
|
||||
```ts
|
||||
class OrderAggregate {
|
||||
private events: OrderEvent[] = [];
|
||||
private state: { items: Item[]; status: 'open' | 'shipped' | 'cancelled' } = {
|
||||
items: [], status: 'open',
|
||||
};
|
||||
|
||||
static fromHistory(events: OrderEvent[]): OrderAggregate {
|
||||
const a = new OrderAggregate();
|
||||
for (const e of events) a.apply(e);
|
||||
return a;
|
||||
}
|
||||
|
||||
private apply(e: OrderEvent) {
|
||||
switch (e.type) {
|
||||
case 'OrderCreated': /* state init */ break;
|
||||
case 'ItemAdded':
|
||||
this.state.items.push({ id: e.data.itemId, qty: e.data.qty });
|
||||
break;
|
||||
case 'OrderShipped':
|
||||
this.state.status = 'shipped';
|
||||
break;
|
||||
case 'OrderCancelled':
|
||||
this.state.status = 'cancelled';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// command → 새 events
|
||||
addItem(itemId: string, qty: number) {
|
||||
if (this.state.status !== 'open') throw new Error('order closed');
|
||||
const e: OrderEvent = { type: 'ItemAdded', data: { orderId: this.id, itemId, qty } };
|
||||
this.apply(e);
|
||||
this.events.push(e);
|
||||
}
|
||||
|
||||
uncommittedEvents(): OrderEvent[] { return this.events; }
|
||||
}
|
||||
```
|
||||
|
||||
### Event store (append-only)
|
||||
```sql
|
||||
CREATE TABLE events (
|
||||
global_seq BIGSERIAL PRIMARY KEY,
|
||||
stream_id TEXT NOT NULL, -- e.g. 'order-42'
|
||||
stream_seq BIGINT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
data JSONB NOT NULL,
|
||||
metadata JSONB,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE (stream_id, stream_seq) -- optimistic concurrency
|
||||
);
|
||||
|
||||
CREATE INDEX events_stream ON events (stream_id, stream_seq);
|
||||
```
|
||||
|
||||
```ts
|
||||
async function append(streamId: string, expectedSeq: number, events: OrderEvent[]) {
|
||||
for (const [i, e] of events.entries()) {
|
||||
await db.events.insert({
|
||||
streamId, streamSeq: expectedSeq + i + 1,
|
||||
type: e.type, data: e.data,
|
||||
}); // unique violation = concurrency 에러 — 재시도
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 로드 + 명령 + 저장
|
||||
```ts
|
||||
async function addItem(orderId: string, itemId: string, qty: number) {
|
||||
const events = await db.events.findStream(`order-${orderId}`);
|
||||
const order = OrderAggregate.fromHistory(events);
|
||||
|
||||
order.addItem(itemId, qty);
|
||||
|
||||
await append(`order-${orderId}`, events.length, order.uncommittedEvents());
|
||||
}
|
||||
```
|
||||
|
||||
### Projection (read model)
|
||||
```ts
|
||||
// 이벤트 흐름 구독 → 일반 테이블에 반영
|
||||
async function project(e: OrderEvent) {
|
||||
switch (e.type) {
|
||||
case 'OrderCreated':
|
||||
await db.ordersView.insert({ id: e.data.orderId, userId: e.data.userId, items: [], status: 'open' });
|
||||
break;
|
||||
case 'ItemAdded':
|
||||
await db.ordersView.update(e.data.orderId, { items: { push: { id: e.data.itemId, qty: e.data.qty } } });
|
||||
break;
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
// global_seq 추적해서 재시작 가능
|
||||
async function startProjector() {
|
||||
let cursor = await db.projectionCursor.get('orders');
|
||||
for await (const e of streamFrom(cursor)) {
|
||||
await project(e);
|
||||
cursor = e.global_seq;
|
||||
await db.projectionCursor.set('orders', cursor);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Snapshot
|
||||
```ts
|
||||
const SNAPSHOT_EVERY = 100;
|
||||
|
||||
async function loadAggregate(streamId: string): Promise<OrderAggregate> {
|
||||
const snap = await db.snapshots.findLatest(streamId);
|
||||
const events = await db.events.findFrom(streamId, snap?.streamSeq ?? 0);
|
||||
const a = OrderAggregate.fromState(snap?.state, events);
|
||||
return a;
|
||||
}
|
||||
```
|
||||
|
||||
### Replay (재구성)
|
||||
```ts
|
||||
// projection 망가지면
|
||||
await db.ordersView.deleteAll();
|
||||
await db.projectionCursor.set('orders', 0);
|
||||
// projector 재시작 → 처음부터 재구성
|
||||
```
|
||||
|
||||
## 🤔 의사결정 기준
|
||||
| 도메인 | 적합 |
|
||||
|---|---|
|
||||
| 금융 / 거래 / 회계 | 매우 적합 |
|
||||
| Audit / compliance 강함 | 적합 |
|
||||
| 워크플로 / 도메인 복잡 | 적합 |
|
||||
| 단순 CRUD | 과잉 — 일반 ORM |
|
||||
| 강력 query (복잡 SQL) | Projection 으로 read 모델 분리 |
|
||||
| 작은 팀 / 빠른 MVP | 비추 |
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **이벤트 schema 변경**: 영원 — 새 event type + upcasting.
|
||||
- **Aggregate 가 외부 호출**: pure 해야. infrastructure 분리.
|
||||
- **Read 를 직접 events**: 항상 projection.
|
||||
- **모든 도메인 ES**: 단순 CRUD 까지 — 학습 곡선만.
|
||||
- **Snapshot 없음 — 큰 stream**: 로드 매번 느림.
|
||||
- **Projection 없이 query**: O(N) 매번.
|
||||
- **Events 삭제 / 수정**: append-only 깨짐. 보상 이벤트.
|
||||
|
||||
## 🤖 LLM 활용 힌트
|
||||
- 복잡 도메인 + audit 강 → ES.
|
||||
- Aggregate / Event / Stream / Projection / Snapshot 5종.
|
||||
- Postgres + JSONB 로 시작 → EventStore DB 또는 Kurrent 로 확장.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[Backend_CQRS_Patterns]]
|
||||
- [[Backend_Saga_Patterns]]
|
||||
- [[Backend_Outbox_Pattern]]
|
||||
Reference in New Issue
Block a user