e2c5471046
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
8.0 KiB
8.0 KiB
id, title, category, status, verification_status, canonical_id, aliases, duplicate_of, source_trust_level, confidence_score, created_at, updated_at, review_reason, merge_history, tags, raw_sources, applied_in, github_commit
| id | title | category | status | verification_status | canonical_id | aliases | duplicate_of | source_trust_level | confidence_score | created_at | updated_at | review_reason | merge_history | tags | raw_sources | applied_in | github_commit | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| event-sourced-store-pattern | 이벤트 소싱 스토어 패턴 | Architecture | draft | applied |
|
A | 0.92 | 2026-06-13 | 2026-06-13 |
|
|
|
이벤트 소싱 스토어 패턴
🎯 한 줄 통찰 (One-line insight)
이벤트 소싱은 "현재 상태를 덮어쓰지 않고 일어난 일(event)을 추가만(append-only) 기록"하는 영속화 방식이며, AstraAI 는 이를 JSONL 파일 + 제네릭 팩토리(createEventStore<E>) 로 구현해 4개 도메인 스토어의 ~240줄 중복을 한 곳으로 통합한다 [S1].
🧠 핵심 개념 (Core concepts)
- Append-only: 데이터를 수정·삭제하지 않고 끝에 줄을 추가만 한다. 이력이 전부 남아 감사·재현·디버깅에 강하다 [S1].
- JSONL (JSON Lines): 한 줄에 JSON 객체 하나. 스트리밍 append 가 쉽고, 한 줄이 손상돼도 나머지는 읽을 수 있다 [S1].
- 이벤트 → 상태 계산: 저장은 이벤트로, 현재 상태 는 이벤트들을 재생(
computeStates)해 도출한다 — 도메인 파일의 책임 [S1]. - 제네릭 팩토리 + 검증 주입: I/O(읽기/추가/카운트)는 공통 모듈이, 도메인 로직은 호출부가 — 관심사 분리 [S1].
- 내결함 파싱: 손상된 줄은 skip 하고 계속 — append-only 라 1줄 손상이 전체를 무력화하면 안 된다 [S1].
🧩 추출된 패턴 (Extracted patterns)
- 중복 4벌 → 제네릭 1벌: customers/hire/runway/feedback 이 같은
getXFilePath/readX/appendX/countX를 반복 →createEventStore<E>로 흡수. BOM/인코딩 등 edge case fix 도 한 번에 전파 [S1]. - 팩토리 함수 + 클로저: 클래스 대신 함수가 내부 함수들을 담은 객체를 반환 — 캡슐화는 클로저로,
new불필요 [S1]. - 타입 가드 검증을 옵션으로:
validate: (e) => e is E를 주입해 파싱 결과의 유효성을 도메인이 정의 [S1]. - 결과를 판별 유니온으로:
append가{ ok: true; filePath } | { ok: false; error }반환 — 워크스페이스 없음 등 흔한 실패를 예외 없이 전달 [S1]. - 워크스페이스 상대경로:
relPath: '.astra/customers.jsonl'를 워크스페이스 루트 기준으로 해석 [S1].
📖 세부 내용 (Details)
왜 이벤트 소싱인가 (이 코드에서)
고객/채용/런웨이/피드백 같은 도메인은 "변경 이력 자체가 가치" 다. 마지막 상태만 저장하면 "언제 무엇이 바뀌었나"를 잃는다. 이벤트를 append 하면 전체 타임라인이 보존되고, 현재 상태는 필요할 때 재생으로 만든다 [S1].
공통 I/O vs 도메인 로직 경계
모듈 헤더 주석이 경계를 명확히 한다: "도메인별 로직(computeStates 등)은 그대로 도메인 파일에 남음 — 본 모듈은 I/O 만 추상화." 즉 createEventStore 는 read/append/count/getFilePath 만 제공하고, "이벤트들로 현재 고객 목록을 만드는" 로직은 customers 도메인에 둔다 [S1].
내결함 읽기
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (!trimmed) continue;
try {
const parsed = JSON.parse(trimmed);
if (opts.validate(parsed)) out.push(parsed);
} catch { /* skip malformed — append-only 라 손상 1줄이 전체 무력화하면 안 됨 */ }
}
한 줄 파싱 실패나 검증 실패는 그 줄만 버리고 계속한다 — 견고성의 핵심 [S1].
안전한 append
fs.mkdirSync(dirname, { recursive: true }) 로 디렉터리 보장 후 appendFileSync(... + '\n'). 실패는 throw 가 아니라 { ok: false, error } 로 반환해 호출부가 사용자에게 안내할 수 있게 한다 [S1].
⚖️ 비교 및 선택 기준 (Comparison & decision criteria)
| 항목 (Option) | 장점 | 단점 | 언제 선택 |
|---|---|---|---|
| Append-only 이벤트(JSONL) | 이력 보존, 내결함, 단순 | 상태 재생 비용, 파일 증가 | 변경 이력이 가치 있을 때 |
| 상태 덮어쓰기(JSON 1개) | 읽기 즉시, 작음 | 이력 손실, 동시쓰기 충돌 | 마지막 값만 중요할 때 |
| SQLite/DB | 쿼리·인덱스·트랜잭션 | 의존성·운영 비용 | 대량·복잡 쿼리 |
⚖️ 모순 및 업데이트 (Contradictions & updates)
- 파일 무한 증가: append-only 는 파일이 계속 커진다. 주기적 compaction(스냅샷 + 이후 이벤트만 유지)이 필요할 수 있다 — 현재 모듈은 compaction 을 제공하지 않으므로 도메인이 관리.
- 동시 append: 단일 프로세스 내 순차 append 는 안전하나, 멀티 프로세스/동시 쓰기는 잠금이 필요하다. AstraAI 는 무거운 작업을 동시성 제어 Lock Queue Transaction 의 lockManager 로 직렬화한다.
🛠️ 적용 사례 (Applied in summary)
AstraAI/src/features/_shared/eventSourcedStore.ts— 제네릭 이벤트 스토어 본체. customers/hire/runway/feedback 도메인이 이를 인스턴스화해 사용 [S1].- 메모리 계층의 episodic/long-term 도 유사하게 버전 필드를 가진 직렬화 스토어 형태(
EpisodicStore { version, episodes, lastUpdated })를 쓴다 [S2].
💻 코드 패턴 (Code patterns)
// 제네릭 이벤트 스토어 (src/features/_shared/eventSourcedStore.ts)
export function createEventStore<E>(opts: EventStoreOptions<E>): EventStore<E> {
function getFilePath(): string | null {
const folders = vscode.workspace.workspaceFolders;
if (!folders?.length) return null;
return path.join(folders[0].uri.fsPath, opts.relPath); // 워크스페이스 상대경로
}
function read(): E[] {
const fp = getFilePath();
if (!fp || !fs.existsSync(fp)) return [];
const out: E[] = [];
for (const line of fs.readFileSync(fp, 'utf-8').split('\n')) {
const t = line.trim(); if (!t) continue;
try { const p = JSON.parse(t); if (opts.validate(p)) out.push(p); }
catch { /* 손상 줄 skip */ }
}
return out;
}
function append(event: E) {
const fp = getFilePath();
if (!fp) return { ok: false, error: '워크스페이스 폴더가 없어 저장 불가.' } as const;
try {
fs.mkdirSync(path.dirname(fp), { recursive: true });
fs.appendFileSync(fp, JSON.stringify(event) + '\n', 'utf-8');
return { ok: true, filePath: fp } as const;
} catch (e: any) { return { ok: false, error: e?.message || String(e) } as const; }
}
return { getFilePath, read, append, count };
}
// 도메인 사용:
const store = createEventStore<CustomerEvent>({
relPath: '.astra/customers.jsonl',
validate: (e): e is CustomerEvent => typeof (e as any).id === 'string',
});
✅ 검증 상태 및 신뢰도
- 상태: draft
- 검증 단계: applied
- 출처 신뢰도: A
- 신뢰 점수: 0.92
- 중복 검사 결과: 신규 생성 (New discovery)
🔗 지식 그래프 (Knowledge Graph)
- 상위/루트: AstraAI 아키텍처 개요
- 관련 개념: TypeScript 고급 타입, 5계층 메모리 시스템, 동시성 제어 Lock Queue Transaction
- 참조 맥락: 로컬 LLM 이 이력·로그·상태를 파일로 영속화하는 스토어를 설계할 때 참조.
📚 출처 (Sources)
- [S1] AstraAI/src/features/_shared/eventSourcedStore.ts — createEventStore 제네릭 팩토리, 내결함 파싱, 판별 유니온 결과
- [S2] AstraAI/src/memory/types.ts — 버전 필드를 가진 직렬화 스토어(EpisodicStore/LongTermStore)
📝 변경 이력 (Change history)
- 2026-06-13: AstraAI 코드 분석 기반 초안 생성.