의존성 주입과 IoC 컨테이너
객체가 구체 의존성을 직접 생성하지 않도록 생성자 주입으로 분리하고 IoC 컨테이너의 등록·해결 흐름을 이해합니다.
객체지향 설계 원칙, 특히 의존성 역전 원칙(DIP)을 지키다 보면 자연스럽게 의존성 주입(Dependency Injection, DI) 개념을 만나게 됩니다.
그리고 규모가 커진 애플리케이션에서 DI를 효율적으로 관리하기 위해 IoC 컨테이너(Inversion of Control Container)를 사용합니다.
이 절에서는 의존성 주입의 개념, 다양한 주입 방식, 그리고 IoC 컨테이너의 역할과 이점에 대해 타입스크립트와 함께 살펴보겠습니다.
SOLID의 ILogger 예제와 생성자·인터페이스가 선수 학습 개념입니다. IoC는 필요한 작업을 언제 호출하거나 객체를 어떻게 연결할지에 관한 제어를 바깥에 맡기는 더 넓은 개념입니다. 프레임워크가 등록된 이벤트 콜백을 호출하는 것도 그 사례입니다. DI는 그중 의존 객체를 외부에서 제공하는 방법이며, IoC 컨테이너는 DI의 조립을 관리하는 도구입니다. 수동 생성자 주입에도 컨테이너 설치는 필요하지 않습니다.
의존성 주입
정의: 의존성 주입은 객체가 자신이 의존하는 객체(의존성)를 직접 생성하거나 찾는 대신, 외부에서 주입받는 설계 패턴입니다.
설명: 일반적으로 객체는 자신이 필요한 다른 객체(의존성)를 직접 생성하거나, 팩토리 메서드를 호출하여 얻어옵니다.
이 경우, 객체와 그 의존성 사이에 강한 결합이 발생합니다.
이는 코드의 재사용성을 저해하고, 테스트를 어렵게 만들며, 변경에 취약하게 만듭니다.
의존성 주입은 이러한 강한 결합을 느슨하게(Loosely Coupled) 만들어줍니다.
객체는 자신이 필요한 것을 주입받으므로, 어떤 구현체를 사용할지 직접 결정하지 않습니다.
대신, 그 결정은 외부(즉, DI를 수행하는 주체)로 역전(Inversion)됩니다.
DI의 주요 이점- 결합도 감소: 객체 생성·선택 책임을 사용하는 객체에서 분리합니다. 아래처럼 사용자가 소유한 인터페이스로 받으면 구체 구현에 대한 소스 의존도 줄일 수 있습니다. 구체 클래스 타입을 주입하는 DI만으로 DIP가 자동 성립하지는 않습니다.
- 재사용성 증가: 의존하는 구현체를 쉽게 교체할 수 있으므로, 동일한 객체를 다른 환경이나 다른 구현체와 함께 재사용하기 용이합니다.
- 테스트 용이성: 유닛 테스트 시 실제 의존성 대신 목(Mock) 객체나 스텁(Stub) 객체를 쉽게 주입하여 테스트 범위를 격리하고 신뢰성을 높일 수 있습니다.
- 유지보수성 및 확장성: 변경에 대한 파급 효과를 줄이고, 새로운 기능을 추가할 때 기존 코드를 수정하는 대신 새로운 구현체를 주입하는 방식으로 확장이 용이해집니다 (OCP, DIP 준수).
이전 10장 1절의 DIP 예시와 유사합니다.
// 1. 추상화 (의존성: ILogger)
interface ILogger {
log(message: string): void;
}
// 2. 구체적인 의존성 구현체 (저수준 모듈)
class ConsoleLogger implements ILogger {
log(message: string): void {
console.log(`[Console Log]: ${message}`);
}
}
class FileLogger implements ILogger {
log(message: string): void {
console.log(`[File Log]: ${message}`);
// 실제 파일 시스템에 쓰는 로직
}
}
// 3. 의존성을 주입받는 객체 (고수준 모듈)
class UserService {
private logger: ILogger; // ILogger 인터페이스에 의존
// 1. 생성자 주입 (Constructor Injection): 가장 일반적이고 권장되는 방식
constructor(logger: ILogger) {
this.logger = logger;
}
// 2. 메서드 주입 (Method Injection): 특정 메서드 실행 시 필요한 의존성
public registerUser(username: string, email: string, logger?: ILogger): void {
const activeLogger = logger ?? this.logger; // 메서드 인자가 우선, 없으면 생성자 주입된 것 사용
activeLogger.log(`Registering user: ${username}, ${email}`);
// 사용자 등록 로직
}
// 3. 속성(Setter) 주입 (Property/Setter Injection): public 속성 또는 setter 메서드를 통해 주입
public setLogger(logger: ILogger): void {
this.logger = logger;
}
public processUserData(data: unknown): void {
this.logger.log(`Processing user data: ${JSON.stringify(data)}`);
// 데이터 처리 로직
}
}
// 사용 예시 (DI 적용)
// 생성자 주입
const consoleLogger = new ConsoleLogger();
const userServiceWithConsole = new UserService(consoleLogger);
userServiceWithConsole.registerUser("Alice", "[email protected]");
userServiceWithConsole.processUserData({ id: 1, name: "Alice" });
// 다른 로거로 쉽게 교체 (테스트 용이성)
const fileLogger = new FileLogger();
const userServiceWithFile = new UserService(fileLogger);
userServiceWithFile.registerUser("Bob", "[email protected]");
// 메서드 주입 예시
userServiceWithConsole.registerUser("Charlie", "[email protected]", fileLogger); // 일시적으로 fileLogger 사용
// 속성 주입 예시
const userServiceWithSetter = new UserService(new ConsoleLogger()); // 일단 ConsoleLogger로 초기화
userServiceWithSetter.setLogger(new FileLogger()); // 나중에 FileLogger로 변경
userServiceWithSetter.processUserData({ id: 3, name: "David" });- 객체 생성 시 생성자를 통해 의존성을 주입받습니다.
- 가장 권장되는 방식입니다. 의존성이 명확하고, 객체가 생성될 때 모든 필수 의존성이 갖춰지도록 강제하므로 불변성을 유지하는 데 유리합니다.
- 타입스크립트에서는 생성자 매개변수에 타입을 명시하여 강력한 타입 검사를 받을 수 있습니다.
- 특정 메서드를 호출할 때 해당 메서드의 매개변수로 의존성을 주입받습니다.
- 해당 메서드에서만 필요한 의존성일 경우, 또는 선택적인 의존성일 경우에 유용합니다.
- 객체의 public 속성 또는 setter 메서드를 통해 의존성을 주입합니다.
- 선택적인 의존성이나, 객체 생성 후 동적으로 의존성을 변경해야 할 때 사용될 수 있습니다. 하지만 객체의 불변성을 해칠 수 있고, 필수 의존성인지 파악하기 어려울 수 있습니다.
의존성이 필수인지, 호출마다 달라지는지, 생성 이후 바뀌어야 하는지를 기준으로 주입 방식을 선택할 수 있습니다.
생성자 주입은 필요한 값을 받는 시점을 정합니다. 이 예제처럼 setLogger가 있으면 생성자에서 받았더라도 이후 값이 바뀝니다. 객체의 불변성까지 필요하다면 변경 메서드를 두지 않는 별도 설계가 필요합니다. DI는 값을 외부에서 넘기는 방식이고 DIP는 코드가 의존하는 추상화의 방향이므로, 구체 클래스 자체를 주입하는 코드가 자동으로 DIP를 만족하는 것은 아닙니다.
호출 범위와 저장된 의존성 확인
FileLogger라는 이름은 원문에서 콘솔 출력 형식을 구별하기 위한 것이며 실제 파일 쓰기는 구현하지 않았습니다. 로그를 눈으로 비교하는 대신 다음 테스트 로거를 같은 수동 DI 예제에 추가해 보세요.
class MemoryLogger implements ILogger {
readonly messages: string[] = [];
log(message: string): void {
this.messages.push(message);
}
}
const defaultLog = new MemoryLogger();
const temporaryLog = new MemoryLogger();
const service = new UserService(defaultLog);
service.registerUser("Eve", "[email protected]", temporaryLog);
service.processUserData({ id: 4 });
service.setLogger(temporaryLog);
service.processUserData({ id: 5 });
console.log(defaultLog.messages);
console.log(temporaryLog.messages);여기서는 JSON으로 직렬화할 수 있는 객체만 예제로 사용합니다. unknown은 입력의 타입을 임의로 가정하지 않게 하지만 순환 참조나 BigInt의 직렬화 성공까지 보장하지 않습니다. readonly messages도 배열 필드의 재대입을 제한할 뿐 push()까지 금지하지 않습니다.
예상되는 배열은 defaultLog에 Processing user data: {"id":4} 한 항목, temporaryLog에 Registering user: Eve, [email protected]과 Processing user data: {"id":5} 두 항목입니다. Eve의 한 호출에 넘긴 로거가 서비스의 필드를 바꾸지 않았으므로 id 4는 처음 로거로 갑니다. setter 호출 뒤 id 5는 저장된 새 로거로 갑니다. JSON.stringify 결과가 메시지 뒤에 붙는 점도 원문에서 확인하세요.
스스로 호출 범위 바꾸기
위 실행을 마친 뒤 service.registerUser("Finn", "[email protected]", defaultLog), service.processUserData({ id: 6 })을 순서대로 호출합니다. 두 배열의 최종 길이와 새 메시지가 들어갈 배열을 예측해 보세요.
로거 선택 풀이
Finn의 호출에만 defaultLog를 넘기므로 그 배열은 길이 2가 됩니다. 필드에는 여전히 temporaryLog가 저장되어 있어 id 6 메시지는 그 배열로 가고 길이는 3이 됩니다. 메서드 인자의 activeLogger는 이번 호출의 지역 변수이며 this.logger를 대입하는 코드는 setter에만 있습니다.
IoC 컨테이너
정의: IoC 컨테이너는 객체의 생성, 구성, 생명 주기를 관리하고, 객체 간의 의존성을 자동으로 주입해주는 프레임워크 또는 라이브러리입니다.
설명: DI 패턴을 직접 구현하다 보면, 애플리케이션의 규모가 커질수록 수많은 객체들을 수동으로 생성하고 의존성을 주입하는 코드가 복잡해집니다.
이때 IoC 컨테이너가 큰 도움이 됩니다.
IoC 컨테이너는 객체 생성 및 의존성 주입의 제어권을 개발자에게서 컨테이너로 넘겨 관리합니다. 이를 제어의 역전(Inversion)이라고 합니다.
개발자는 런타임 token과 provider를 등록하고 해당 token의 객체를 요청합니다. TypeScript interface는 실행 시 사라지므로 이 예제는 Symbol을 token으로 사용합니다.
컨테이너는 등록된 정보를 바탕으로 필요한 의존성을 찾아 자동으로 주입해줍니다.
IoC 컨테이너의 주요 역할객체 생성 및 관리: 객체를 필요할 때 생성하고, 싱글턴 등 객체의 생명 주기를 관리합니다.
의존성 해결 및 주입: 등록된 정보를 바탕으로 특정 객체가 필요로 하는 의존성(다른 객체들)을 자동으로 찾아내어 주입합니다.
구성 관리: 객체들의 관계와 설정을 한곳에서 중앙 집중식으로 관리합니다.
자바스크립트/타입스크립트 생태계에는 다양한 IoC 컨테이너 라이브러리가 존재합니다.
대표적인 예시는 다음과 같습니다.
- InversifyJS: 타입스크립트 기반의 IoC 컨테이너로, 데코레이터(Decorator)를 사용하여 의존성 주입을 선언적으로 표현할 수 있게 합니다.
- TypeDI: 데코레이터를 사용하여 DI를 쉽게 구현할 수 있도록 돕는 또 다른 인기 있는 라이브러리입니다.
- NestJS (프레임워크): 자체적으로 강력한 IoC 컨테이너를 내장하고 있어, DI를 기반으로 한 아키텍처를 쉽게 구축할 수 있습니다.
import "reflect-metadata"; // 이 Inversify 레거시 데코레이터 예제의 메타데이터 지원
import { Container, injectable, inject } from "inversify";
// 1. 심볼(Symbol) 또는 문자열을 사용하여 서비스(인터페이스)를 식별합니다.
const TYPES = {
ILogger: Symbol.for("ILogger"),
UserService: Symbol.for("UserService"),
};
// 2. 추상화 (인터페이스)
interface ILogger {
log(message: string): void;
}
// 3. 구현체 (InversifyJS의 @injectable 데코레이터 사용)
@injectable()
class ConsoleLogger implements ILogger {
log(message: string): void {
console.log(`[Console Log by Inversify]: ${message}`);
}
}
@injectable()
class FileLogger implements ILogger {
log(message: string): void {
console.log(`[File Log by Inversify]: ${message}`);
}
}
// 4. 의존성을 주입받는 클래스 (InversifyJS의 @inject 데코레이터 사용)
@injectable()
class UserService {
private logger: ILogger;
// 생성자 주입을 @inject 데코레이터로 명시
constructor(@inject(TYPES.ILogger) logger: ILogger) {
this.logger = logger;
}
registerUser(username: string): void {
this.logger.log(`User registered: ${username}`);
}
}
// 5. IoC 컨테이너 설정 (바인딩)
const container = new Container();
// ILogger 심볼에 ConsoleLogger 구현체를 바인딩
container.bind<ILogger>(TYPES.ILogger).to(ConsoleLogger);
// FileLogger를 사용하려면 위 ConsoleLogger 바인딩 줄 대신 아래 줄을 활성화합니다.
// container.bind<ILogger>(TYPES.ILogger).to(FileLogger);
// UserService 심볼에 UserService 클래스를 바인딩
container.bind<UserService>(TYPES.UserService).to(UserService);
// 6. 컨테이너를 통해 객체 요청 (주입은 컨테이너가 알아서 처리)
const userService = container.get<UserService>(TYPES.UserService);
userService.registerUser("Grace");
// 런타임에 로거 구현체를 변경하고 싶다면, 바인딩만 변경하면 됩니다.
// container.rebind<ILogger>(TYPES.ILogger).to(FileLogger);
// const anotherUserService = container.get<UserService>(TYPES.UserService);
// anotherUserService.registerUser("Heidi");reflect-metadata: 이 Inversify 예제에서는 타입스크립트의 레거시 데코레이터 설정인experimentalDecorators와emitDecoratorMetadata를 모두true로 설정합니다.@injectable(): 이 데코레이터는 클래스가 IoC 컨테이너에 의해 관리될 수 있음을 나타냅니다.@inject(TYPES.ILogger): 생성자 매개변수에 이 데코레이터를 붙여주면, InversifyJS 컨테이너는TYPES.ILogger심벌에 바인딩된 구현체를 찾아UserService의 생성자로 주입해줍니다.container.bind<ILogger>(TYPES.ILogger).to(ConsoleLogger): 런타임 식별자TYPES.ILogger가 요청될 때 제공할 클래스를 등록합니다.<ILogger>는 정적 타입 검사에 쓰이며 실행 시 token을 대신하지 않습니다.container.get<UserService>(TYPES.UserService): 컨테이너에게UserService인스턴스를 요청합니다. 컨테이너는UserService가ILogger를 필요로 한다는 점을 파악하고, 바인딩된ConsoleLogger를 자동 생성해UserService에 주입한 뒤 최종 인스턴스를 반환합니다.
IoC 컨테이너는 token 그래프와 scope를 순서대로 해석한다 — 상태 추적
토큰 등록 뒤 get 시 의존성 탐색과 생성/캐시/객체 반환 상태가 달라진다. 컨테이너와 객체 정체성으로 추적할 가치가 있다.
IOC / REGISTER → RESOLVE
등록을 마친 컨테이너가 token 의존을 풀어 객체를 반환한다
bootstrap은 token/provider를 등록하고 caller는 root token을 요청합니다. 이후 의존 조회와 scope 판단은 컨테이너 내부에서 이루어지며, bootstrap에 다시 의존 정보를 요청하지 않습니다.
REGISTER
런타임 token을 등록
interface는 JS에서 사라집니다. 교재는 TYPES.ILogger·TYPES.UserService Symbol을 구현체에 바인딩합니다.
RESOLVE
의존 token을 해석
get(UserService)를 받으면 생성자의 @inject(TYPES.ILogger) 메타데이터를 읽고 ConsoleLogger를 주입합니다.
SCOPE
재사용 여부는 수명 정책
transient는 새 값을, singleton은 같은 값을 재사용합니다. request scope는 같은 root resolve 요청 안의 재사용입니다. HTTP 요청 수명과 자동으로 같지는 않습니다.
LESSON SCOPE
교재 바인딩은 기본 수명
예제는 singleton/request scope를 지정하지 않았습니다. scope cache가 항상 사용되는 실행 결과처럼 읽지 않습니다. 라이브러리 버전·데코레이터 설정도 함께 확인합니다.
같은 계약의 구현 선택과 cache 적용 범위를 구별하고, 등록되지 않은 token은 실제 해결 오류로 확인합니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 관계·조건 설명 | 모바일의 첫 흐름은 Bootstrap이 token과 provider를 Container에 등록하는 순서를 보여 준다. 이어지는 두 번째 흐름에서 Caller의 root 요청을 해결한다. |
| 원도표 설명 | interface는 JS에서 사라집니다. 교재는 TYPES.ILogger·TYPES.UserService Symbol을 구현체에 바인딩합니다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| binding 등록 | container: bindings 있음 / 객체: 아직 생성 안 될 수 있음 | 토큰을 구현 생성 규칙과 scope에 연결한다 | 등록과 실제 객체 생성을 구분한다 |
| get과 의존 탐색 | 요청: UserService / 의존: ILogger binding | 컨테이너가 UserService 요청을 받아 ILogger 의존 토큰을 해석한다 | 실제 생성자는 해석된 객체를 받는다 |
| scope 적용 | 객체 identity: scope 규칙에 따름 / consumer: 주입된 실제 객체 | scope에 따라 기존 객체를 재사용하거나 새로 만든 뒤 반환한다 | 토큰이라는 런타임 interface 객체를 생성하는 것은 아니다 |
IoC 컨테이너는 token 그래프와 scope를 순서대로 해석한다 — 상태 추적
토큰 등록 뒤 get 시 의존성 탐색과 생성/캐시/객체 반환 상태가 달라진다. 컨테이너와 객체 정체성으로 추적할 가치가 있다.
IOC / REGISTER → RESOLVE
등록을 마친 컨테이너가 token 의존을 풀어 객체를 반환한다
bootstrap은 token/provider를 등록하고 caller는 root token을 요청합니다. 이후 의존 조회와 scope 판단은 컨테이너 내부에서 이루어지며, bootstrap에 다시 의존 정보를 요청하지 않습니다.
REGISTER
런타임 token을 등록
interface는 JS에서 사라집니다. 교재는 TYPES.ILogger·TYPES.UserService Symbol을 구현체에 바인딩합니다.
RESOLVE
의존 token을 해석
get(UserService)를 받으면 생성자의 @inject(TYPES.ILogger) 메타데이터를 읽고 ConsoleLogger를 주입합니다.
SCOPE
재사용 여부는 수명 정책
transient는 새 값을, singleton은 같은 값을 재사용합니다. request scope는 같은 root resolve 요청 안의 재사용입니다. HTTP 요청 수명과 자동으로 같지는 않습니다.
LESSON SCOPE
교재 바인딩은 기본 수명
예제는 singleton/request scope를 지정하지 않았습니다. scope cache가 항상 사용되는 실행 결과처럼 읽지 않습니다. 라이브러리 버전·데코레이터 설정도 함께 확인합니다.
같은 계약의 구현 선택과 cache 적용 범위를 구별하고, 등록되지 않은 token은 실제 해결 오류로 확인합니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 관계·조건 설명 | 모바일의 첫 흐름은 Bootstrap이 token과 provider를 Container에 등록하는 순서를 보여 준다. 이어지는 두 번째 흐름에서 Caller의 root 요청을 해결한다. |
| 원도표 설명 | interface는 JS에서 사라집니다. 교재는 TYPES.ILogger·TYPES.UserService Symbol을 구현체에 바인딩합니다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| binding 등록 | container: bindings 있음 / 객체: 아직 생성 안 될 수 있음 | 토큰을 구현 생성 규칙과 scope에 연결한다 | 등록과 실제 객체 생성을 구분한다 |
| get과 의존 탐색 | 요청: UserService / 의존: ILogger binding | 컨테이너가 UserService 요청을 받아 ILogger 의존 토큰을 해석한다 | 실제 생성자는 해석된 객체를 받는다 |
| scope 적용 | 객체 identity: scope 규칙에 따름 / consumer: 주입된 실제 객체 | scope에 따라 기존 객체를 재사용하거나 새로 만든 뒤 반환한다 | 토큰이라는 런타임 interface 객체를 생성하는 것은 아니다 |
그림의 scope cache는 재사용하도록 설정된 수명 정책에 해당합니다. 원문은 기본 바인딩만 보여주며 singleton/request scope를 설정한 예가 아닙니다.
라이브러리 예제를 따라갈 때의 확인 순서
Inversify 블록은 수동 DI 예제와 이름이 겹치므로 별도 파일과 프로젝트에서 사용합니다. interface 타입은 실행 시 token이 될 수 없어 TYPES의 Symbol과 생성자 @inject가 연결을 설명하는 핵심입니다. 먼저 ILogger·UserService의 두 등록이 완료된 뒤 get을 호출하는 순서를 확인하세요. ILogger 등록을 빼면 주입에 필요한 의존성을 해결할 수 없으며, 완성 객체를 반환했다는 결과를 기대하면 안 됩니다.
2026년 10월 9일 확인한 공식 시작 안내와 Container API의 대상은 8.x입니다. 이 예제에는 experimentalDecorators와 emitDecoratorMetadata를 설정하고 reflect-metadata를 불러옵니다. 이는 Inversify의 레거시 생성자 매개변수 데코레이터 구성으로, TypeScript의 모든 데코레이터에 이 라이브러리가 필요하다는 뜻이 아닙니다.
8.x의 기본 scope는 transient이고 get은 하나의 일치하는 동기 바인딩을 요구합니다. 같은 token을 bind로 두 번 등록하면 기본 get이 임의로 마지막 로거를 고르는 것이 아니라 오류가 됩니다. 위 대체 바인딩 주석은 두 줄을 동시에 활성화하라는 뜻이 아닙니다. 현재 8.x의 rebind(...).to(...)는 동기 교체이며, rebindAsync(...)는 Promise를 반환합니다. 비동기 정리·생성 조건이 있다면 해당 API를 기다리고 getAsync 등 맞는 해결 경로를 선택합니다. 라이브러리를 설치해 이 교재를 실행한 결과는 아니므로 실제 설치 버전·설정과 타입 검사·실행 결과는 따로 확인합니다.
한 번 resolve해 받은 userService의 logger 필드는 그때 주입한 객체를 가리킵니다. 나중에 컨테이너의 ILogger 바인딩을 바꾼다고 이미 반환된 서비스의 필드가 저절로 교체되지는 않습니다. 새 root를 resolve할지 기존 객체를 명시적으로 변경할지는 객체 수명과 조립 경계의 별도 결정입니다.
의존성 주입과 IoC 컨테이너의 장단점
장점- 느슨한 결합: 객체 간의 의존성을 분리하여 코드의 유연성과 재사용성을 극대화합니다.
- 테스트 용이성: 목 객체를 쉽게 주입하여 단위 테스트를 간결하게 만듭니다.
- 확장성: 새로운 구현체가 추가되어도 기존 코드를 수정할 필요 없이 설정을 통해 변경할 수 있습니다.
- 중앙 집중식 관리: 모든 의존성 관계와 객체 생명 주기를 한곳에서 관리하여 코드베이스 이해도를 높입니다.
- 초기 학습 곡선: DI 패턴과 IoC 컨테이너의 개념 및 사용법을 익히는 데 시간이 필요할 수 있습니다.
- 런타임 오버헤드: 컨테이너가 객체를 동적으로 생성하고 의존성을 해결하는 과정에서 런타임 오버헤드가 발생할 수 있습니다. 객체 그래프 크기·해결 빈도·수명 정책에 따라 비용이 달라지므로 실제 사용 경로를 측정합니다.
- 설정 복잡성: 대규모 프로젝트에서는 컨테이너 설정 파일이나 바인딩 코드가 복잡해질 수 있습니다.
조립 경계 검토
DI / OBJECT COMPOSITION
조립 코드가 로거를 선택해 UserService에 주입한다
생성자 주입에서는 UserService가 로거를 직접 만들지 않습니다. 바깥에서 Console/File/test 로거를 고르고 ILogger 계약으로 넘깁니다. 컨테이너는 이 조립을 관리하는 선택 가능한 도구입니다.
REQUIRED
생성자 주입
필수 logger를 객체 생성 때 넘깁니다. 서비스는 ILogger 계약을 사용하며 어떤 구체 로거를 만들지 선택하지 않습니다.
PER CALL
메서드·setter 주입
교재 registerUser의 선택 인자가 있으면 그 logger를 이번 호출에 사용합니다. setLogger는 저장된 의존성을 바꿉니다. 두 변경 범위를 구별합니다.
OPTIONAL TOOL
수동 조립도 DI다
작은 그래프는 new UserService(logger)로 충분합니다. IoC 도구를 쓰면 등록·해결·수명 관리 책임을 조립 경계에 모을 수 있습니다.
ISOLATION
실제 Inversify 예제의 경계
앞 예제의 UserService에는 @inject와 Symbol token이 있습니다. 컨테이너 관련 선언까지 완전히 분리한 구조는 아니므로 필요한 격리 수준을 선택합니다.
그림의 화살표는 객체 생성·주입입니다. 소스 의존 방향을 설명하는 DIP 그림과 구별합니다.
위 그림은 컨테이너 의존을 조립 경계로 제한하는 설계 방향입니다. 앞의 Inversify 예제는 UserService 생성자에 @inject와 token을 사용하므로 그림처럼 컨테이너 관련 선언까지 완전히 분리한 구현은 아닙니다. 컨테이너 없는 생성자 주입 예제와 비교해 필요한 격리 수준을 선택합니다.
등록 위치, 객체 수명, 테스트 대역이 같은 계약을 따르는지 확인합니다. 컨테이너는 DI의 필수 조건이 아니며, 작은 객체 그래프는 수동 조립으로도 충분합니다.
token·수명·조립 실패를 구별하는 연습
- 등록에
const registered = Symbol("ILogger"), 조회에 별도로 만든const requested = Symbol("ILogger")를 썼습니다. 설명 문자열이 같으니 같은 의존성을 찾을 수 있습니까? 하나의 공유 token 상수를 쓰면 무엇이 달라집니까? - 위 기본 바인딩에서
get(TYPES.UserService)를 두 번 호출한 뒤 로거 바인딩을 바꿉니다. 기존 서비스의 로거까지 바뀝니까? singleton을 따로 설정했다면 새 root를 요청하는 것만으로 모든 의존성이 새로 조립됩니까? TYPES.ILogger에 ConsoleLogger와 FileLogger를 둘 다 기본bind로 등록했습니다. 기본get(TYPES.UserService)는 어느 것을 선택해야 합니까? 한 구현만 쓰려는 목표와 여러 구현을 모두 받으려는 목표를 나누어 설명하세요.
token과 객체 수명 풀이
- 두
Symbol()값은 설명 문자열이 같아도 서로 다른 식별자입니다. 등록과 조회가 같은 공유 상수를 사용해야 합니다. 예제의Symbol.for("ILogger")는 같은 전역 symbol registry 키를 조회하지만, 어느 방식이든 등록자와 주입 선언이 동일한 token을 사용한다는 계약이 필요합니다. TypeScript interface 이름은 실행 중 존재하는 조회 키가 아닙니다. - 기본 transient 클래스 바인딩에서는 각 root 해결에 새 서비스와 로거가 만들어집니다. 이미 받은 서비스는 기존 로거 참조를 계속 가지므로 바인딩 변경만으로 필드가 바뀌지 않습니다. 서비스 자체가 singleton이면 다시
get해도 기존 서비스가 반환될 수 있습니다. 어느 바인딩에 어떤 scope를 지정했는지 확인하고, 새 그래프 생성 또는 명시적 교체를 설계해야 합니다. - 두 바인딩이 일치하면 기본
get은 하나로 결정할 수 없어 오류입니다. 하나만 사용할 때는 기존 바인딩을 교체하거나 선택 조건을 명시합니다. 모두 필요하면getAll또는 명시적인 다중 주입처럼 여러 값을 받는 계약을 설계합니다. 오류를 숨기고 임의 선택하면 구성 실수를 발견하기 어렵습니다.