---
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 코드 분석 기반 초안 생성.
