wiki: Topic_Blog 신규 문서 일괄 추가 + ASTRA 성장 자산 동기화

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Antigravity Agent
2026-06-16 09:55:38 +09:00
parent d77ff5c625
commit e2c5471046
444 changed files with 88916 additions and 231 deletions
@@ -0,0 +1,104 @@
---
id: astraai-architecture-overview
title: "AstraAI 아키텍처 개요"
category: "Architecture"
status: "draft"
verification_status: "applied"
canonical_id: ""
aliases: ["Astra", "AstraAI", "feature-based architecture", "전체 구조", "모듈 아키텍처", "제2뇌 OS"]
duplicate_of: ""
source_trust_level: "S"
confidence_score: 0.95
created_at: 2026-06-13
updated_at: 2026-06-13
review_reason: ""
merge_history: []
tags: ["architecture", "astraai", "feature-based", "layering", "overview"]
raw_sources: ["AstraAI/src/extension.ts", "AstraAI/src 트리(308 TS 파일)", "AstraAI/package.json", "AstraAI/src/core/services.ts"]
applied_in: ["AstraAI"]
github_commit: ""
---
# [[AstraAI 아키텍처 개요]]
## 🎯 한 줄 통찰 (One-line insight)
AstraAI(Astra)는 "로컬 LLM 을 두뇌로 쓰는 VS Code 확장형 자기진화 지식 OS"이며, **기능별 폴더 경계 + 얇은 entry point + 핵심 인프라(core) 위에 도메인(features)을 쌓는 계층형 모듈 아키텍처**로 308개 파일을 조직한다 [S1][S2].
## 🧠 핵심 개념 (Core concepts)
1. **Feature-based 구조:** `src/features/` 아래 각 폴더가 독립 기능(stocks, calendar, company, datacollect, devilAgent…). 기능 간 결합을 최소화하고, 공통 인프라만 `core/`/`lib/` 에서 공유 [S2].
2. **계층 분리:** `core`(인프라) → `lib`/`memory`/`retrieval`/`intelligence`(역량) → `features`(기능) → `extension.ts`(조립). 아래 계층은 위를 모른다 [S2].
3. **얇은 entry point:** `extension.ts``activate()`*조립과 등록만* 한다 — 객체 생성, 의존성 주입, 명령 등록, 워처 시작. 로직은 전부 모듈에 위임 [S1].
4. **의존성 주입:** 생성자에 옵션 객체로 협력자를 주입(`new AgentExecutor(context, { lmStudioStreamer, approvalQueue, ... })`) — 테스트·교체 용이 [S1].
5. **싱글톤 인프라:** 프로세스 전역이 자연스러운 자원(`lockManager`, `actionQueue`)은 모듈 싱글톤으로 export [S1].
## 🧩 추출된 패턴 (Extracted patterns)
- **God-class 분해:** 거대해지는 orchestrator(`agent.ts`)를 `handlePrompt/`, `llm/`, `actions/`, `multiAgent/`, `sessions/` 하위 모듈로 쪼개 함수 단위로 추출하고, orchestrator 는 이들을 호출만 한다 [참조: [[Agent 오케스트레이터 분해]]].
- **인터페이스 우선 서비스:** `IAIService`/`IBrainService` 인터페이스를 두고 구현(`AIService`)을 분리 — 엔진 폴백 같은 정책을 구현체에 캡슐화 [S3].
- **자기등록 핸들러:** slashRouter 에 핸들러가 side-effect import 로 자기 등록 — 새 명령 추가 시 중앙 등록표를 건드리지 않음 [S1].
- **부트스트랩 분리:** 활성화 시 필요한 초기화(brain 디렉터리, 임베딩 감지, 기능 인벤토리)를 `extension/*Bootstrap.ts` 로 분리해 entry point 를 얇게 유지 [S1].
- **disposable 수명 관리:** 생성한 모든 자원을 `context.subscriptions.push(...)` 로 등록해 확장 종료 시 일괄 정리 [S1].
## 📖 세부 내용 (Details)
### 디렉터리 지도 (역할별)
- **core/** — 횡단 인프라: `lock`, `queue`, `transaction`, `errors`, `errorHandler`, `services`(AI/Brain 서비스), `session`, `events`, `health`, `telemetry`, `statusBar`.
- **memory/** — 5계층 인지 메모리(Short/Long/Project/Procedural/Episodic) + `distillation`. → [[5계층 메모리 시스템]].
- **retrieval/** — RAG 파이프라인: `chunker`, `scoring`(TF-IDF), `embeddings`, `brainIndex`, `contextBudget`, rerank 류. → [[RAG 검색 파이프라인]].
- **intelligence/** — 검증·자기평가: `criticAgent`, `confidenceEngine`, `correctionLoop`, `epistemicGuardBlock`, `knowledgeValidation`. → [[Intelligence 검증 레이어]].
- **lib/contextBuilders/** — 프롬프트에 들어갈 컨텍스트 블록을 조립하는 순수 함수 모음(메모리·프로젝트·일정·자기인식 등 30+개).
- **features/** — 도메인 기능. 각자 `index.ts`(공개 API) + store + handler + prompt 로 구성되는 경향.
- **agent/** + **agents/** — 에이전트 실행 세부와 다중 에이전트 워크플로 매니저.
- **integrations/** — 외부 연동(telegram).
- **sidebar/** + **sidebarProvider.ts** — 웹뷰 UI 와 메시지 핸들러.
- **system/**, **lmstudio/**, **scaffolder/**, **skills/** — 시스템 사양 탐지, LM Studio SDK 수명관리, 프로젝트 스캐폴딩, 스킬 로딩.
### 제어 흐름 (한 턴의 처리)
1. 웹뷰(사이드바)에서 사용자 입력 → `sidebar/chatHandlers``AgentExecutor`.
2. `agent/handlePrompt/*` 가 컨텍스트 블록(메모리·RAG·프로젝트·일정)을 조립.
3. 모델 라우팅: 클라우드 prefix 면 provider 어댑터, 아니면 로컬 엔진(LM Studio/Ollama). → [[LLM 프로바이더 추상화]].
4. 스트리밍 응답 → 후처리(sanitize, devil rebuttal) → post-answer hooks(critic 검수, 교정 캡처).
5. 세션 종료 시 메모리 추출/증류, 성장 사이클 워처가 폐루프로 학습.
### 빌드/런타임
- esbuild 단일 번들(`out/extension.js`), `vscode` external. 런타임 의존성은 `@lmstudio/sdk`, `pdf-parse` 둘뿐 — 나머지는 native API(fetch 등)로 자급 [S2].
## ⚖️ 모순 및 업데이트 (Contradictions & updates)
- **단일 거대 agent.ts(1681줄):** 분해를 했음에도 orchestrator 가 여전히 크다. 이는 "한 턴의 제어 흐름을 한 곳에서 읽을 수 있게" 하려는 의도적 트레이드오프 — 세부는 추출하되 흐름의 골격은 남긴다.
- **features 간 결합:** 일부 기능(company, stocks)은 텔레그램·시트 등 여러 통합을 동시에 참조해 완전 독립은 아니다. core/lib 를 통한 간접 결합으로 완화.
## 🛠️ 적용 사례 (Applied in summary)
- `AstraAI/src/extension.ts``activate()` 전체가 "얇은 조립 entry point" 의 교과서적 예 — 25개+ 모듈을 생성·주입·등록하지만 비즈니스 로직은 0 [S1].
## 💻 코드 패턴 (Code patterns)
```typescript
// activate(): 조립과 등록만 — 로직은 모듈에 위임 (src/extension.ts 발췌)
export async function activate(context: vscode.ExtensionContext) {
await ensureDefaultBrainConfigured(); // 부트스트랩 분리
void ensureEmbeddingConfigured(context); // 비차단 best-effort
initAstraPathResolver(context);
const lmStudioClient = new LMStudioClient(getConfig().ollamaUrl);
const agent = new AgentExecutor(context, { // 생성자 의존성 주입
onStreamLifecycle: { start: () => lifecycle.onStreamStart(), end: () => lifecycle.onStreamEnd() },
lmStudioStreamer, approvalQueue,
});
const provider = new SidebarChatProvider(context.extensionUri, context, agent, { lifecycle, /* ... */ });
context.subscriptions.push( // 수명 관리: 종료 시 일괄 dispose
vscode.commands.registerCommand('g1nation.openChat', () => provider.openAsPanel(vscode.ViewColumn.Three)),
...registerProviderCommands(context, { getProvider: () => provider }),
);
}
```
## 🔗 지식 그래프 (Knowledge Graph)
- **상위/루트:** [[AstraAI 아키텍처 개요]]
- **관련 개념:** [[VSCode 확장 구조와 생명주기]], [[의존성 주입과 서비스 인터페이스]], [[Agent 오케스트레이터 분해]], [[모듈 시스템과 프로젝트 구성]]
- **참조 맥락:** 로컬 LLM 이 새 기능을 "어느 계층/폴더에 어떤 형태로" 추가할지 판단할 때 최상위 지도로 참조.
## 📚 출처 (Sources)
- [S1] AstraAI/src/extension.ts — activate/deactivate, 조립·주입·등록·부트스트랩
- [S2] AstraAI/src 디렉터리 트리(308 TS 파일) + package.json — 계층/번들 구성
- [S3] AstraAI/src/core/services.ts — IAIService/IBrainService 인터페이스 우선 설계
## 📝 변경 이력 (Change history)
- 2026-06-13: AstraAI 전체 코드 분석 기반 초안 생성.
@@ -0,0 +1,122 @@
---
id: vscode-extension-structure-lifecycle
title: "VSCode 확장 구조와 생명주기"
category: "Architecture"
status: "draft"
verification_status: "applied"
canonical_id: ""
aliases: ["VS Code Extension", "activate", "deactivate", "Disposable", "Webview", "command 등록", "extension API"]
duplicate_of: ""
source_trust_level: "A"
confidence_score: 0.93
created_at: 2026-06-13
updated_at: 2026-06-13
review_reason: ""
merge_history: []
tags: ["vscode", "extension", "lifecycle", "disposable", "webview", "astraai"]
raw_sources: ["AstraAI/src/extension.ts", "AstraAI/package.json", "AstraAI/src/features/_shared/eventSourcedStore.ts"]
applied_in: ["AstraAI"]
github_commit: ""
---
# [[VSCode 확장 구조와 생명주기]]
## 🎯 한 줄 통찰 (One-line insight)
VS Code 확장은 `activate(context)` 에서 깨어나 명령·뷰·이벤트를 등록하고 `deactivate()` 에서 정리되며, 모든 자원을 **`context.subscriptions` 에 Disposable 로 등록**해 두면 확장 종료 시 메모리/타이머/리스너가 누수 없이 일괄 정리된다 [S1].
## 🧠 핵심 개념 (Core concepts)
1. **`activate(context)`:** 확장 진입점. `package.json` 의 activationEvents 조건이 충족되면 호출된다. `context: ExtensionContext` 가 전역 상태·구독·시크릿 저장소를 제공 [S1].
2. **`deactivate()`:** 종료 훅. 명시적으로 풀어야 하는 자원(외부 프로세스, 봇, 모델 언로드)을 여기서 정리 [S1].
3. **Disposable 패턴:** `{ dispose(): void }` 를 가진 객체. `context.subscriptions.push(disposable)` 하면 VS Code 가 종료 시 `dispose()` 를 호출 [S1].
4. **Command:** `vscode.commands.registerCommand(id, handler)` 로 등록, `package.json``contributes.commands` 에 선언. 반환값이 Disposable [S1].
5. **Webview:** HTML 기반 커스텀 UI. 확장↔웹뷰는 `postMessage` 로 통신. AstraAI 는 사이드바 대신 *에디터 컬럼* 에 패널로 띄운다 [S1].
6. **Configuration:** `vscode.workspace.getConfiguration('g1nation')` 으로 설정 읽기, `onDidChangeConfiguration` 으로 변경 반응 [S1].
7. **SecretStorage:** 토큰 등 민감정보는 `context.secrets` 에 저장(평문 설정 금지) [S1].
## 🧩 추출된 패턴 (Extracted patterns)
- **모든 자원을 subscriptions 로:** 명령·리스너·상태바·타이머·커스텀 dispose 콜백까지 전부 `context.subscriptions.push(...)` — 정리를 잊지 않는 구조적 보장 [S1].
- **설정 변경 반응:** `onDidChangeConfiguration((e) => { if (!e.affectsConfiguration('g1nation.ollamaUrl')) return; ... })` — 관심 키만 필터링해 재설정 [S1].
- **lazy webview 전송:** 매우 이른 활성화 시점엔 웹뷰가 아직 없을 수 있어 `provider?._sendModels(...)` 옵셔널 체이닝 + best-effort [S1].
- **getter 콜백으로 늦은 바인딩:** 아직 안 만들어진 객체는 `getProvider: () => provider` 처럼 getter 로 주입 — 순환 의존/초기화 순서 문제 회피 [S1].
- **워처 = disposable 반환 함수:** `startStocksWatcher(context)` 가 타이머를 만들고 disposable 을 반환해 subscriptions 에 등록 → 종료 시 timer cleanup [S1].
## 📖 세부 내용 (Details)
### activate 의 책임 순서 (AstraAI 실제 순서)
1. 가시화(버전 popup/console) → 환경 진단.
2. 부트스트랩(brain 디렉터리, 임베딩 감지, 기능 인벤토리) — 일부는 `void` 비차단.
3. 인프라 초기화(health monitor, path resolver, config 검증).
4. 핵심 객체 생성·주입(LMStudioClient, lifecycle, AgentExecutor, SidebarChatProvider, BridgeServer).
5. 명령·리스너 등록.
6. 워처 시작(stocks, daily briefing, growth cycle, sleep digest).
7. 웹뷰 자동 오픈.
### deactivate 의 책임
`HealthCheckMonitor.dispose()`, 인메모리 인덱스 해제(`clearBrainTokenIndex()` — Map 이 프로세스 수명 동안 안 비는 것 방지), 텔레그램 봇 stop, 모델 lifecycle dispose+unload. *명시적 정리가 필요한 것만* 여기서 처리하고, subscriptions 로 등록된 것은 VS Code 가 알아서 정리한다 [S1].
### 설정·시크릿
```typescript
const cfg = vscode.workspace.getConfiguration('g1nation');
const timeout = cfg.get<number>('lmStudio.idleTimeoutMs', 300000); // 기본값 동반
const token = await context.secrets.get(TELEGRAM_TOKEN_SECRET_KEY); // 민감정보는 secrets
```
### Webview 통신
확장→웹뷰는 `webview.postMessage({ type: 'streamChunk', value })`, 웹뷰→확장은 메시지 핸들러(`sidebar/chatHandlers`). 스트리밍은 `streamStart`/`streamChunk`/`streamEnd` 의 메시지 프로토콜로 표현 [참조: [[Agent 오케스트레이터 분해]]].
## ⚖️ 모순 및 업데이트 (Contradictions & updates)
- **사이드바 뷰 제거(v2.81):** 과거엔 activity bar 사이드바에 webview view 를 등록했으나, 현재는 에디터 컬럼 3에 패널로 띄운다. 대신 activity bar 에는 명령 링크만 있는 빈 TreeView 를 둔다 — 탭이 닫혀도 다시 열 수 있게 [S1].
- **activationEvents:** 너무 광범위하면(예: `*`) 시작이 느려진다. 필요한 이벤트로 좁히는 것이 좋다(AstraAI 는 명령/뷰 기반).
## 🛠️ 적용 사례 (Applied in summary)
- `AstraAI/src/extension.ts` — activate/deactivate 의 전 과정, subscriptions 등록, 설정 반응, secrets 사용, 워처 등록이 모두 한 파일에 [S1].
- `AstraAI/src/features/_shared/eventSourcedStore.ts``vscode.workspace.workspaceFolders` 로 워크스페이스 경로 해석(확장에서 파일 경로 다루는 표준 방식) [S3].
## 💻 코드 패턴 (Code patterns)
```typescript
// 1) 명령 등록 → 반환된 Disposable 을 subscriptions 로 (src/extension.ts)
context.subscriptions.push(
vscode.commands.registerCommand('g1nation.clearChat', () => provider.clearChat()),
);
// 2) 설정 변경 반응 — 관심 키만 (src/extension.ts)
context.subscriptions.push(
vscode.workspace.onDidChangeConfiguration((e) => {
if (!e.affectsConfiguration('g1nation.ollamaUrl')) return;
const newUrl = vscode.workspace.getConfiguration('g1nation').get<string>('ollamaUrl', '');
lmStudioClient.setBaseUrl(newUrl);
}),
);
// 3) 커스텀 dispose 콜백도 disposable 로 등록
context.subscriptions.push({ dispose: () => activityTracker.dispose() });
// 4) getter 콜백으로 늦은 바인딩 (순환/초기화 순서 회피)
const telegramBot = createTelegramBot(context, { telegramClient, getProvider: () => provider });
// 5) deactivate — 명시 정리가 필요한 것만
export async function deactivate() {
HealthCheckMonitor.dispose();
clearBrainTokenIndex();
if (_telegramBot) { try { await _telegramBot.stop(); } catch {} }
}
```
## ✅ 검증 상태 및 신뢰도
- **상태:** draft
- **검증 단계:** applied
- **출처 신뢰도:** A
- **신뢰 점수:** 0.93
- **중복 검사 결과:** 신규 생성 (New discovery)
## 🔗 지식 그래프 (Knowledge Graph)
- **상위/루트:** [[AstraAI 아키텍처 개요]]
- **관련 개념:** [[의존성 주입과 서비스 인터페이스]], [[모듈 시스템과 프로젝트 구성]], [[동시성 제어 Lock Queue Transaction]]
- **참조 맥락:** 로컬 LLM 이 VS Code 확장의 명령/뷰/설정/수명관리 코드를 작성·수정할 때 참조.
## 📚 출처 (Sources)
- [S1] AstraAI/src/extension.ts — activate/deactivate, subscriptions, 명령/설정/시크릿/웹뷰/워처
- [S2] AstraAI/package.json — engines.vscode, main, contributes(명령/뷰)
- [S3] AstraAI/src/features/_shared/eventSourcedStore.ts — workspaceFolders 경로 해석
## 📝 변경 이력 (Change history)
- 2026-06-13: AstraAI 코드 분석 기반 초안 생성.
@@ -0,0 +1,129 @@
---
id: concurrency-lock-queue-transaction
title: "동시성 제어 Lock Queue Transaction"
category: "Architecture"
status: "draft"
verification_status: "applied"
canonical_id: ""
aliases: ["concurrency", "AsyncLock", "mutex", "race condition", "동시성", "큐", "concurrency limit", "트랜잭션"]
duplicate_of: ""
source_trust_level: "A"
confidence_score: 0.94
created_at: 2026-06-13
updated_at: 2026-06-13
review_reason: ""
merge_history: []
tags: ["architecture", "concurrency", "lock", "queue", "transaction", "astraai"]
raw_sources: ["AstraAI/src/core/lock.ts", "AstraAI/src/core/queue.ts", "AstraAI/src/core/transaction.ts"]
applied_in: ["AstraAI"]
github_commit: ""
---
# [[동시성 제어 Lock Queue Transaction]]
## 🎯 한 줄 통찰 (One-line insight)
단일 스레드 JavaScript 도 `await` 사이에 다른 작업이 끼어들어 *경쟁 상태(race condition)* 가 생기며, AstraAI 는 **자원별 비동기 락(AsyncLock)·동시성 제한 큐·파일 보상 트랜잭션** 세 가지로 "한 번에 하나만·자원을 폭주시키지 않고·실패하면 되돌리는" 동시성 안전을 확보한다 [S1][S2][S3].
## 🧠 핵심 개념 (Core concepts)
1. **JS 의 동시성:** 단일 스레드라도 `await` 지점에서 제어가 넘어가므로, 같은 파일을 동시에 읽고-수정-쓰면 갱신 손실이 난다. 직렬화가 필요 [S1].
2. **비동기 뮤텍스 (AsyncLock):** 자원 ID 별로 "이전 작업이 끝나야 다음이 시작"되는 Promise 체인. `acquire` 가 release 함수를 반환, `try/finally` 로 반드시 해제 [S1].
3. **데드락 방지 타임아웃:** 락 대기를 `Promise.race([previous, timeout])` 로 감싸 무한 대기 차단 [S1].
4. **동시성 제한 큐:** 동시에 실행되는 작업 수를 `max(2, cpus-1)` 로 제한해 I/O·메모리 폭주 방지 [S2].
5. **보상 트랜잭션:** 파일시스템에 트랜잭션이 없으므로, 변경 전 백업→실패 시 복원으로 원자성을 흉내 [S3].
## 🧩 추출된 패턴 (Extracted patterns)
- **고유 토큰으로 안전한 정리:** 각 락 entry 에 `Symbol` 토큰을 부여하고, cleanup/release 시 "내 토큰이 아직 Map 의 최신인지" 확인 후에만 삭제 — 연쇄 호출 시 다른 작업 entry 를 지우는 race 방지 [S1].
- **release 는 반드시 try/finally:** `const release = await lock.acquire(id); try { ... } finally { release(); }` — 예외가 나도 락이 풀리게 [S1].
- **enqueue 가 Promise 반환:** `enqueue<T>(task)` 가 작업 결과 Promise 를 돌려주되 실행은 슬롯이 빌 때 — 호출부는 평소처럼 await [S2].
- **micro-delay 로 숨통:** 무거운 I/O 사이 `await sleep(10)` 로 시스템에 여유 [S2].
- **begin/record/commit/rollback:** 변경 전 `record(파일)` 로 백업, 성공 `commit`(백업 폐기), 실패 `rollback`(원복) [S3].
## 📖 세부 내용 (Details)
### AsyncLock 의 핵심 — Promise 체인 + 토큰
```typescript
const previousPromise = this.locks.get(resourceId)?.promise ?? Promise.resolve();
const token = Symbol(`lock:${resourceId}`);
let release!: () => void;
const newPromise = new Promise<void>((resolve) => { release = resolve; });
this.locks.set(resourceId, { promise: previousPromise.then(() => newPromise), token });
await Promise.race([previousPromise, timeoutPromise]); // 앞 작업 끝날 때까지(타임아웃 보호)
return () => { release(); if (this.locks.get(resourceId)?.token === token) this.locks.delete(resourceId); };
```
**버그 사후기록(주석)**: 옛 구현은 `.then(...)` 이 매번 새 Promise 를 반환해 동일성 비교가 *항상 false* → cleanup 실패. 또 release 시 무조건 `delete` 해서 연쇄 호출 시 다른 작업 entry 를 지우는 race. → 각 entry 에 고유 symbol 을 부여하고 "내 토큰이 최신일 때만" 정리하도록 수정 [S1].
### 동시성 제한 큐
```typescript
function defaultConcurrencyLimit() { return Math.max(2, (os.cpus()?.length ?? 4) - 1); } // UI 스레드 여유
public async enqueue<T>(task: () => Promise<T>): Promise<T> {
return new Promise<T>((resolve, reject) => {
this.queue.push(async () => { try { resolve(await task()); } catch (e) { reject(e); } });
this.processNext();
});
}
```
`processNext``activeCount < limit` 일 때만 다음 작업을 꺼내 실행하고, `finally` 에서 `activeCount--` 후 재귀로 다음을 당긴다. 무거운 LLM 호출은 큐가 아니라 `missionId` 락으로 직렬화하므로 큐는 단순하게 유지 [S2].
### 보상 트랜잭션 (파일 원자성)
`isTransactionActive` 가드로 중복 begin 방지. `record` 는 같은 파일을 한 번만 백업(이미 있으면 skip). rollback 은 `created` 파일은 삭제, `modified` 는 원본 내용으로 복원. 외부 API 호출 성공 여부도 `recordExternalAction` 으로 추적해 `isFullyVerified()` 로 전체 검증 [S3].
## ⚖️ 비교 및 선택 기준 (Comparison & decision criteria)
| 도구 | 막는 문제 | 사용 시점 |
|---|---|---|
| AsyncLock (자원별) | 같은 자원 동시 수정(갱신 손실) | 파일/세션 등 공유 자원 read-modify-write |
| 동시성 제한 큐 | 자원 폭주(메모리/IO/소켓) | 대량 작업을 일정 동시성으로 처리 |
| 보상 트랜잭션 | 부분 실패로 인한 불일치 | 여러 파일을 한 단위로 변경 |
| 외부 abort signal | 취소 불가 | 사용자 Stop / 타임아웃 ([[비동기 프로그래밍 Promise async await]]) |
## ⚖️ 모순 및 업데이트 (Contradictions & updates)
- **락 타임아웃의 부작용:** 타임아웃으로 깨어난 작업은 자원을 못 얻은 채 throw 한다. 호출부는 이 실패를 재시도/사용자 안내로 처리해야 한다(조용히 진행 금지).
- **큐의 static 동시성:** 동적 조정이 없어 부하 급변에 둔감하지만, 무거운 작업이 락으로 직렬화되므로 큐는 단순함을 택했다 — 의도적 단순성.
- **메모리 내 트랜잭션:** 백업이 메모리(Map)에 있어 프로세스가 죽으면 롤백이 불가능하다. 진짜 내구성이 필요하면 WAL/DB 가 필요.
## 🛠️ 적용 사례 (Applied in summary)
- `AstraAI/src/core/lock.ts``lockManager` 싱글톤. agent 가 파일/세션 작업 직렬화에 사용 [S1].
- `AstraAI/src/core/queue.ts``actionQueue` 싱글톤. 대량 액션 처리 [S2].
- `AstraAI/src/core/transaction.ts` — AgentExecutor 가 파일 다중 변경을 묶을 때 [S3].
## 💻 코드 패턴 (Code patterns)
```typescript
// 1) 락 — 반드시 try/finally 로 해제 (src/core/lock.ts)
const release = await lockManager.acquire(filePath);
try {
const cur = fs.readFileSync(filePath, 'utf-8');
fs.writeFileSync(filePath, transform(cur)); // read-modify-write 직렬화
} finally {
release(); // 예외가 나도 반드시 해제
}
// 2) 동시성 제한 큐 (src/core/queue.ts)
const result = await actionQueue.enqueue(() => heavyTask(item)); // 슬롯 빌 때 실행
// 3) 보상 트랜잭션 (src/core/transaction.ts)
tx.begin();
try {
await tx.record(fileA); fs.writeFileSync(fileA, nextA);
await tx.record(fileB); fs.writeFileSync(fileB, nextB);
tx.commit();
} catch (e) { tx.rollback(); throw e; }
```
## ✅ 검증 상태 및 신뢰도
- **상태:** draft
- **검증 단계:** applied
- **출처 신뢰도:** A
- **신뢰 점수:** 0.94
- **중복 검사 결과:** 신규 생성 (New discovery)
## 🔗 지식 그래프 (Knowledge Graph)
- **상위/루트:** [[AstraAI 아키텍처 개요]]
- **관련 개념:** [[비동기 프로그래밍 Promise async await]], [[에러 처리와 커스텀 에러]], [[이벤트 소싱 스토어 패턴]]
- **참조 맥락:** 로컬 LLM 이 공유 자원·대량 작업·다중 파일 변경의 동시성 안전 코드를 작성할 때 참조.
## 📚 출처 (Sources)
- [S1] AstraAI/src/core/lock.ts — AsyncLockManager(토큰 기반), race 타임아웃, 버그 사후기록
- [S2] AstraAI/src/core/queue.ts — ActionQueueManager 동시성 제한
- [S3] AstraAI/src/core/transaction.ts — 보상 트랜잭션(begin/record/commit/rollback)
## 📝 변경 이력 (Change history)
- 2026-06-13: AstraAI 코드 분석 기반 초안 생성.
@@ -0,0 +1,128 @@
---
id: dependency-injection-service-interface
title: "의존성 주입과 서비스 인터페이스"
category: "Architecture"
status: "draft"
verification_status: "applied"
canonical_id: ""
aliases: ["DI", "dependency injection", "인터페이스", "service interface", "느슨한 결합", "testability", "전략 패턴"]
duplicate_of: ""
source_trust_level: "A"
confidence_score: 0.93
created_at: 2026-06-13
updated_at: 2026-06-13
review_reason: ""
merge_history: []
tags: ["architecture", "dependency-injection", "interface", "design-pattern", "astraai"]
raw_sources: ["AstraAI/src/core/services.ts", "AstraAI/src/extension.ts", "AstraAI/src/intelligence/criticAgent.ts", "AstraAI/src/agent/multiAgent/workflow.ts"]
applied_in: ["AstraAI"]
github_commit: ""
---
# [[의존성 주입과 서비스 인터페이스]]
## 🎯 한 줄 통찰 (One-line insight)
의존성 주입은 "객체가 협력자를 *직접 만들지 않고 밖에서 받는*" 설계이며, AstraAI 는 **인터페이스로 계약을 정의하고, 생성자 옵션 객체·함수 타입으로 구현을 주입**해 모듈을 순수하고 교체·테스트 가능하게 만든다 [S1][S3].
## 🧠 핵심 개념 (Core concepts)
1. **인터페이스 = 계약:** `IAIService`/`IBrainService` 는 "무엇을 할 수 있는가"만 정의하고 "어떻게" 는 구현에 맡긴다. 호출부는 인터페이스에만 의존 [S1].
2. **생성자 주입 (Constructor injection):** 협력자를 `new X(deps)` 처럼 생성 시점에 옵션 객체로 받는다 — 객체가 자기 의존을 숨기지 않고 시그니처에 드러낸다 [S2].
3. **함수 주입 (Function injection):** 무거운 의존(LLM 호출)을 *함수 타입* 으로 받아, 모듈 자체는 LLM 을 모른 채 순수 함수로 남는다 (`CritiqueLlmCall`) [S3].
4. **getter 주입 (Lazy):** 아직 생성되지 않은 의존은 `getProvider: () => provider` 처럼 getter 로 — 초기화 순서/순환 의존 회피 [S2].
5. **deps 번들 객체:** 여러 협력자를 `WorkflowDeps` 같은 인터페이스로 묶어 한 번에 전달 [S4].
## 🧩 추출된 패턴 (Extracted patterns)
- **인터페이스 선언 → 구현 클래스:** `interface IAIService { ... }` + `class AIService implements IAIService { ... }`. 정책(엔진 폴백)은 구현에 캡슐화, 호출부는 인터페이스만 본다 [S1].
- **옵션 객체로 생성자 주입:** `new AgentExecutor(context, { onStreamLifecycle, lmStudioStreamer, approvalQueue })` — 위치 인자 대신 명명 옵션으로 가독성·확장성 확보 [S2].
- **함수 타입으로 LLM 추상화:** `type CritiqueLlmCall = (system, user, maxTokens) => Promise<string>` 를 주입받아, criticAgent 는 어떤 엔진이든 무관하게 동작하고 테스트 시 가짜 함수를 끼운다 [S3].
- **deps 인터페이스 + 콜백 게터:** `WorkflowDeps { getWebview, getAbortSignal, chatHistory, ... }` — 동적으로 바뀌는 상태는 게터로 전달해 stale 참조 방지 [S4].
- **싱글톤 vs 주입 구분:** 프로세스 전역 자원(lock/queue)은 싱글톤, 정책·상태를 가진 협력자는 주입 [S2].
## 📖 세부 내용 (Details)
### 왜 인터페이스인가
`AIService` 는 "LM Studio 먼저, 실패 시 Ollama 폴백, 빈 응답은 soft failure" 같은 *정책* 을 담는다. 호출부(텔레그램 핸들러 등)는 `IAIService.chat()` 만 알면 되고, 정책이 바뀌어도 호출부는 안 바뀐다. 이것이 "구현이 아니라 추상에 의존하라"(DIP)의 실천 [S1].
### 함수 주입으로 순수성 유지
`criticAgent.ts` 헤더 주석은 "모든 LLM 의존은 주입(critique caller) — 모듈 자체는 순수, 테스트 가능" 이라고 명시한다. `runCriticReview({ ..., callLlm })` 는 실제 LLM 을 직접 부르지 않고 주입된 `callLlm` 을 부른다. 덕분에:
- 프로덕션: `agent.ts``callNonStreaming` 을 주입.
- 테스트: 고정 문자열을 반환하는 가짜 함수를 주입 → LLM 없이 단위 테스트 [S3].
### deps 번들 + getter 의 이유
멀티에이전트 워크플로는 실행 도중 webview/abort signal 이 바뀔 수 있다. 그래서 값이 아니라 *게터* 를 주입한다:
```typescript
export interface WorkflowDeps {
getWebview: () => vscode.Webview | undefined; // 호출 시점의 최신 webview
getAbortSignal: () => AbortSignal | undefined; // 새 controller 의 최신 signal
chatHistory: ChatMessage[];
}
```
주석은 "호출자가 stop()+new AbortController() 를 먼저 마쳐야 한다 — getAbortSignal() 은 그 새 controller 의 signal 을 반환해야 함" 이라고 함정을 경고한다 [S4].
## ⚖️ 비교 및 선택 기준 (Comparison & decision criteria)
| 항목 (Option) | 장점 | 단점 | 언제 선택 |
|---|---|---|---|
| 생성자 주입(옵션 객체) | 의존 명시, 교체 쉬움 | 생성 코드 장황 | 상태/정책 가진 협력자 |
| 함수 주입 | 모듈 순수, 테스트 최상 | 콜백 시그니처 관리 | LLM·I/O 같은 외부 효과 |
| getter 주입 | 늦은 바인딩, 순환 회피 | 호출 시점 의존 | 동적/초기화 순서 문제 |
| 싱글톤 import | 간결, 전역 공유 | 테스트 격리 어려움 | 프로세스 전역 인프라(lock/queue) |
## ⚖️ 모순 및 업데이트 (Contradictions & updates)
- **DI 컨테이너 없음:** AstraAI 는 별도 DI 프레임워크를 쓰지 않고 *수동 주입* (activate 에서 직접 조립)한다. 규모가 작고 조립 지점이 한 곳이라 프레임워크 비용이 불필요 — 큰 시스템이면 컨테이너가 유리할 수 있다.
- **싱글톤의 비용:** `lockManager`/`actionQueue` 싱글톤은 편하지만 테스트 격리를 어렵게 한다. AstraAI 는 전역성이 본질인 자원에만 한정해 사용한다.
## 🛠️ 적용 사례 (Applied in summary)
- `AstraAI/src/core/services.ts` — IAIService/IBrainService 인터페이스 + 구현 [S1].
- `AstraAI/src/extension.ts` — AgentExecutor/SidebarChatProvider 생성자 옵션 주입, getProvider 게터 [S2].
- `AstraAI/src/intelligence/criticAgent.ts` — CritiqueLlmCall 함수 주입 [S3].
- `AstraAI/src/agent/multiAgent/workflow.ts` — WorkflowDeps 게터 번들 [S4].
## 💻 코드 패턴 (Code patterns)
```typescript
// 1) 인터페이스 계약 + 구현 분리 (src/core/services.ts)
export interface IAIService {
call(prompt: string): Promise<string>;
chat(req: AIChatRequest): Promise<AIChatResult>;
}
export class AIService implements IAIService { /* 엔진 폴백 정책 캡슐화 */ }
// 2) 생성자 옵션 객체 주입 (src/extension.ts)
const agent = new AgentExecutor(context, {
onStreamLifecycle: { start: () => lifecycle.onStreamStart(), end: () => lifecycle.onStreamEnd() },
lmStudioStreamer, approvalQueue,
});
// 3) 함수 주입으로 순수 모듈 (src/intelligence/criticAgent.ts)
export type CritiqueLlmCall = (system: string, user: string, maxTokens: number) => Promise<string>;
export async function runCriticReview(params: { /* ... */ callLlm: CritiqueLlmCall }) {
const raw = await params.callLlm(system, user, opts.maxTokens); // 어떤 엔진인지 모름
return parseCritique(raw);
}
// 4) getter 번들 (src/agent/multiAgent/workflow.ts)
export interface WorkflowDeps {
getWebview: () => vscode.Webview | undefined;
getAbortSignal: () => AbortSignal | undefined;
}
```
## ✅ 검증 상태 및 신뢰도
- **상태:** draft
- **검증 단계:** applied
- **출처 신뢰도:** A
- **신뢰 점수:** 0.93
- **중복 검사 결과:** 신규 생성 (New discovery)
## 🔗 지식 그래프 (Knowledge Graph)
- **상위/루트:** [[AstraAI 아키텍처 개요]]
- **관련 개념:** [[TypeScript 고급 타입]], [[Intelligence 검증 레이어]], [[VSCode 확장 구조와 생명주기]]
- **참조 맥락:** 로컬 LLM 이 협력 객체를 가진 모듈을 테스트 가능하고 교체 가능하게 설계할 때 참조.
## 📚 출처 (Sources)
- [S1] AstraAI/src/core/services.ts — IAIService/IBrainService + AIService 구현
- [S2] AstraAI/src/extension.ts — 생성자 옵션 주입, getProvider 게터
- [S3] AstraAI/src/intelligence/criticAgent.ts — CritiqueLlmCall 함수 주입(순수 모듈)
- [S4] AstraAI/src/agent/multiAgent/workflow.ts — WorkflowDeps 게터 번들
## 📝 변경 이력 (Change history)
- 2026-06-13: AstraAI 코드 분석 기반 초안 생성.
@@ -0,0 +1,135 @@
---
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", "astraai"]
raw_sources: ["AstraAI/src/features/_shared/eventSourcedStore.ts", "AstraAI/src/memory/types.ts"]
applied_in: ["AstraAI"]
github_commit: ""
---
# [[이벤트 소싱 스토어 패턴]]
## 🎯 한 줄 통찰 (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].
### 내결함 읽기
```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 는 안전하나, 멀티 프로세스/동시 쓰기는 잠금이 필요하다. 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)
```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)
- **상위/루트:** [[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 코드 분석 기반 초안 생성.