--- 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` 를 주입받아, 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; chat(req: AIChatRequest): Promise; } 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; 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 코드 분석 기반 초안 생성.