---
id: event-sourced-store-pattern
title: "이벤트 소싱 스토어 패턴"
category: "Architecture"
status: "draft"
verification_status: "applied"
canonical_id: ""
aliases: ["event sourcing", "append-only", "JSONL", "이벤트 스토어", "팩토리 함수", "computeStates"]
duplicate_of: ""
source_trust_level: "A"
confidence_score: 0.92
created_at: 2026-06-13
updated_at: 2026-06-13
review_reason: ""
merge_history: []
tags: ["architecture", "event-sourcing", "persistence", "factory", "connectai"]
raw_sources: ["ConnectAI/src/features/_shared/eventSourcedStore.ts", "ConnectAI/src/memory/types.ts"]
applied_in: ["ConnectAI"]
github_commit: ""
---

# [[이벤트 소싱 스토어 패턴]]

## 🎯 한 줄 통찰 (One-line insight)
이벤트 소싱은 "현재 상태를 덮어쓰지 않고 *일어난 일(event)을 추가만(append-only)* 기록"하는 영속화 방식이며, ConnectAI 는 이를 **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].

### 내결함 읽기
```typescript
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 는 안전하나, 멀티 프로세스/동시 쓰기는 잠금이 필요하다. ConnectAI 는 무거운 작업을 [[동시성 제어 Lock Queue Transaction]] 의 lockManager 로 직렬화한다.

## 🛠️ 적용 사례 (Applied in summary)
- `ConnectAI/src/features/_shared/eventSourcedStore.ts` — 제네릭 이벤트 스토어 본체. customers/hire/runway/feedback 도메인이 이를 인스턴스화해 사용 [S1].
- 메모리 계층의 episodic/long-term 도 유사하게 버전 필드를 가진 직렬화 스토어 형태(`EpisodicStore { version, episodes, lastUpdated }`)를 쓴다 [S2].

## 💻 코드 패턴 (Code patterns)
```typescript
// 제네릭 이벤트 스토어 (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)
- **상위/루트:** [[ConnectAI 아키텍처 개요]]
- **관련 개념:** [[TypeScript 고급 타입]], [[5계층 메모리 시스템]], [[동시성 제어 Lock Queue Transaction]]
- **참조 맥락:** 로컬 LLM 이 이력·로그·상태를 파일로 영속화하는 스토어를 설계할 때 참조.

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

## 📝 변경 이력 (Change history)
- 2026-06-13: ConnectAI 코드 분석 기반 초안 생성.
