e2c5471046
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
8.2 KiB
8.2 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 | |||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| dependency-injection-service-interface | 의존성 주입과 서비스 인터페이스 | Architecture | draft | applied |
|
A | 0.93 | 2026-06-13 | 2026-06-13 |
|
|
|
의존성 주입과 서비스 인터페이스
🎯 한 줄 통찰 (One-line insight)
의존성 주입은 "객체가 협력자를 직접 만들지 않고 밖에서 받는" 설계이며, AstraAI 는 인터페이스로 계약을 정의하고, 생성자 옵션 객체·함수 타입으로 구현을 주입해 모듈을 순수하고 교체·테스트 가능하게 만든다 [S1][S3].
🧠 핵심 개념 (Core concepts)
- 인터페이스 = 계약:
IAIService/IBrainService는 "무엇을 할 수 있는가"만 정의하고 "어떻게" 는 구현에 맡긴다. 호출부는 인터페이스에만 의존 [S1]. - 생성자 주입 (Constructor injection): 협력자를
new X(deps)처럼 생성 시점에 옵션 객체로 받는다 — 객체가 자기 의존을 숨기지 않고 시그니처에 드러낸다 [S2]. - 함수 주입 (Function injection): 무거운 의존(LLM 호출)을 함수 타입 으로 받아, 모듈 자체는 LLM 을 모른 채 순수 함수로 남는다 (
CritiqueLlmCall) [S3]. - getter 주입 (Lazy): 아직 생성되지 않은 의존은
getProvider: () => provider처럼 getter 로 — 초기화 순서/순환 의존 회피 [S2]. - 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 이 바뀔 수 있다. 그래서 값이 아니라 게터 를 주입한다:
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)
// 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 코드 분석 기반 초안 생성.