Files
2nd/10_Wiki/Topic_Programming/Architecture/이벤트_소싱_스토어_패턴.md
T
Antigravity Agent e2c5471046 wiki: Topic_Blog 신규 문서 일괄 추가 + ASTRA 성장 자산 동기화
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 09:55:38 +09:00

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
event sourcing
append-only
JSONL
이벤트 스토어
팩토리 함수
computeStates
A 0.92 2026-06-13 2026-06-13
architecture
event-sourcing
persistence
factory
astraai
AstraAI/src/features/_shared/eventSourcedStore.ts
AstraAI/src/memory/types.ts
AstraAI

이벤트 소싱 스토어 패턴

🎯 한 줄 통찰 (One-line insight)

이벤트 소싱은 "현재 상태를 덮어쓰지 않고 일어난 일(event)을 추가만(append-only) 기록"하는 영속화 방식이며, AstraAI 는 이를 JSONL 파일 + 제네릭 팩토리(createEventStore<E>) 로 구현해 4개 도메인 스토어의 ~240줄 중복을 한 곳으로 통합한다 [S1].

🧠 핵심 개념 (Core concepts)

  1. Append-only: 데이터를 수정·삭제하지 않고 끝에 줄을 추가만 한다. 이력이 전부 남아 감사·재현·디버깅에 강하다 [S1].
  2. JSONL (JSON Lines): 한 줄에 JSON 객체 하나. 스트리밍 append 가 쉽고, 한 줄이 손상돼도 나머지는 읽을 수 있다 [S1].
  3. 이벤트 → 상태 계산: 저장은 이벤트로, 현재 상태 는 이벤트들을 재생(computeStates)해 도출한다 — 도메인 파일의 책임 [S1].
  4. 제네릭 팩토리 + 검증 주입: I/O(읽기/추가/카운트)는 공통 모듈이, 도메인 로직은 호출부가 — 관심사 분리 [S1].
  5. 내결함 파싱: 손상된 줄은 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)

📚 출처 (Sources)

  • [S1] AstraAI/src/features/_shared/eventSourcedStore.ts — createEventStore 제네릭 팩토리, 내결함 파싱, 판별 유니온 결과
  • [S2] AstraAI/src/memory/types.ts — 버전 필드를 가진 직렬화 스토어(EpisodicStore/LongTermStore)

📝 변경 이력 (Change history)

  • 2026-06-13: AstraAI 코드 분석 기반 초안 생성.