9148c358d0
Topic_Agent/Topic_Blog/Topics/Topics_Biz/Topics_Meeting/Topics_Rag의 마크다운 지식 문서를 Topic_General/Topic_Programming/Topic_Graphic/Topic_Business 4개 카테고리로 재분류. - 중복 제거: frontmatter의 status:duplicate/merged + duplicate_of/redirect_to 필드로 자기 자신을 중복으로 선언한 리다이렉트 stub 1032개 제거, 완전 동일 내용 파일 472개 제거, 동일 파일명·다른 내용 충돌 시 더 큰(완전한) 버전만 유지(162개 제거) — 총 1639개 중복 제거. - 분류: 폴더 단위로 명확한 항목(AI_and_ML/Coding/Architecture 등 → Programming, Comfyui/Visual_Effects → Graphic, Topics_Biz/Topics_Meeting/사업 등 → Business, Poetic_Blog_Writing/창의성/Game_Design 등 → General)은 폴더 우선순위로, 나머지 혼재 폴더(Topic_Agent/Topic_Blog/Topics 루트/Thinking & Reasoning/Other/UI_UX_Assets)는 title/tags 키워드 스코어링으로 파일 단위 분류(불명확한 경우 General로 폴백). 원본 폴더명은 "From_*" 서브폴더로 보존해 추적 가능성 유지. - 최종 배치: Programming 2784 / General 1608 / Graphic 285 / Business 249 = 4926개 문서. - 에이전트 운영 상태(.astra/.agent/.obsidian/sessions/memory/_company/docs/lessons/_shared/src)는 지식 콘텐츠가 아니므로 재분류 대상에서 제외하고 원위치 유지. - Topics/Topic_email(상위 보호 폴더 Topic_email과 파일명 100% 중복) 삭제 — 보호 폴더 자체는 미변경. - 완전히 비게 된 Topic_Agent/Topic_Blog/Topics_Biz/Topics_Rag 폴더 제거.
5.7 KiB
5.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 | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| backend-event-sourcing | Event Sourcing — 이벤트 기반 상태 | Coding | draft | B | conceptual | 2026-05-09 | 2026-05-09 |
|
|
|
Event Sourcing
상태 = 모든 이벤트의 fold. 현재 상태 저장 X — 이벤트 append-only 저장. 재구성 / time-travel / audit 자연. 단 학습 곡선 + 복잡 — 모든 도메인에 안 어울림.
📖 핵심 개념
- Event: 과거 fact (
OrderCreated,ItemAdded). - Aggregate: 일관성 경계 + 이벤트 producer.
- Stream: aggregate 의 이벤트 list.
- Projection: 이벤트 → read model 만들기.
- Snapshot: 빠른 복원 위해 N 이벤트마다.
💻 코드 패턴
Event 정의
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)
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)
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);
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 에러 — 재시도
}
}
로드 + 명령 + 저장
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)
// 이벤트 흐름 구독 → 일반 테이블에 반영
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
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 (재구성)
// 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 로 확장.