TypeScript에서 InversifyJS 또는 TSyringe를 사용한 IoC 마스터링
James Reed
Infrastructure Engineer · Leapcell

소개
현대 소프트웨어 개발의 복잡한 세계에서 강력하고 유지보수 가능한 애플리케이션을 구축하는 것은 종종 코드베이스의 다른 부분 간의 종속성을 얼마나 효과적으로 관리하는지에 달려 있습니다. TypeScript가 강력한 타이핑과 확장성으로 인해 계속해서 인기를 얻고 있으므로, 애플리케이션을 느슨하게 결합하고 쉽게 테스트할 수 있도록 보장하는 것이 무엇보다 중요합니다. 이것이 바로 Inversion of Control(IoC)의 개념이 진정으로 빛을 발하는 곳입니다. IoC, 특히 Dependency Injection(DI) 프레임워크를 통해 구현되는 것은 구성 요소가 자체적으로 종속성을 생성하는 대신 외부 엔티티가 종속성을 관리하도록 함으로써 구성 요소를 분리할 수 있게 합니다. 이 패러다임 전환은 모듈성을 크게 향상시키고, 단위 테스트를 용이하게 하며, 궁극적으로 더 탄력적이고 적응 가능한 소프트웨어로 이어집니다. TypeScript 생태계에서 InversifyJS와 TSyringe는 IoC를 구현하기 위한 우아한 솔루션을 제공하며 이러한 기능을 수행하는 강력한 도구로 두드러집니다. 이 기사에서는 IoC의 핵심 원칙을 살펴보고 이 두 프레임워크가 개발자가 더 나은 TypeScript 애플리케이션을 구축할 수 있도록 어떻게 지원하는지 살펴보겠습니다.
Inversion of Control 및 Dependency Injection 이해
InversifyJS와 TSyringe의 구체적인 내용에 대해 자세히 알아보기 전에, 이러한 프레임워크가 활용하는 기본 개념에 대한 명확한 이해를 확립해 보겠습니다.
**Inversion of Control(IoC)**은 프로그램 제어 흐름이 역전되는 디자인 원칙입니다. 애플리케이션이 재사용 가능한 라이브러리를 호출하는 대신, 프레임워크가 애플리케이션의 구성 요소를 호출합니다. 이를 할리우드 원칙으로 생각하십시오. "우리를 부르지 마세요. 우리가 당신을 부를 것입니다." 구성 요소 상호 작용의 맥락에서, 이는 구성 요소가 종속성을 생성하거나 관리하지 않고 외부 메커니즘이 이를 제공한다는 것을 의미합니다.
**Dependency Injection(DI)**은 IoC의 특정 구현입니다. 이는 객체가 의존하는 다른 객체를 받는 기술입니다. 이러한 "종속성"은 자체적으로 생성하거나 서비스 로케이터에서 검색하는 대신 런타임에 종속 객체에 주입됩니다. 종속성이 주입될 수 있는 몇 가지 방법이 있습니다.
- 생성자 주입: 종속성은 클래스 생성자를 통해 제공됩니다. 이는 객체가 모든 필요한 종속성과 함께 유효한 상태로 생성되도록 보장하므로 종종 선호됩니다.
- 세터 주입: 종속성은 공개 세터 메서드를 통해 제공됩니다. 이를 통해 선택적 종속성을 사용하거나 객체 생성 후 종속성을 수정할 수 있습니다.
- 인터페이스 주입: 종속성은 클라이언트 클래스가 구현하는 인터페이스를 통해 제공됩니다. (다른 언어에 비해 TypeScript에서는 덜 일반적입니다.)
DI의 이점은 상당합니다.
- 느슨한 결합: 구성 요소는 구현에 단단히 결합되지 않아 변경하거나 교체하기 쉽습니다.
- 테스트 용이성: 실제 구현 대신 테스트 대상을 주입할 수 있으므로 단위 테스트를 위한 종속성 모의화가 간단해집니다.
- 유지보수성: 명확한 종속성 관계 덕분에 코드를 이해하고 유지보수하기 쉬워집니다.
- 재사용성: 구성 요소는 종속성을 하드코딩하지 않으므로 다른 컨텍스트에서 더 재사용 가능해집니다.
이러한 개념을 염두에 두고 InversifyJS와 TSyringe가 TypeScript에서 이러한 원칙을 구현하는 데 어떻게 도움이 되는지 살펴보겠습니다.
InversifyJS 심층 분석
InversifyJS는 TypeScript 및 JavaScript 애플리케이션을 위한 강력하고 매우 인기 있는 IoC 컨테이너입니다. 데코레이터를 사용하고 메타데이터를 유형화하여 종속성을 정의하고 주입합니다.
핵심 원칙 및 설정
InversifyJS는 세 가지 주요 개념으로 작동합니다.
- 인터페이스/유형: 서비스 계약을 정의합니다. 이는 강력한 타이핑과 느슨한 결합을 촉진합니다.
- 클래스(구현): 해당 인터페이스의 구체적인 구현을 제공합니다.
- 컨테이너: 인터페이스를 해당 구체적인 구현에 바인딩하고 인스턴스를 해결하는 곳입니다.
설치:
npm install inversify reflect-metadata npm install @types/reflect-metadata --save-dev
또한 tsconfig.json
에서 emitDecoratorMetadata
및 experimentalDecorators
를 활성화해야 합니다.
{ "compilerOptions": { "target": "es6", "module": "commonjs", "lib": ["es6"], "emitDecoratorMetadata": true, "experimentalDecorators": true, "sourceMap": true, "outDir": "./dist" } }
애플리케이션 진입점에서 reflect-metadata
를 한 번 가져와야 합니다.
import "reflect-metadata";
예제 구현
로깅 서비스가 필요한 간단한 애플리케이션을 구축한다고 가정해 보겠습니다.
1. 인터페이스/유형 정의:
InversifyJS에서 인터페이스 자체와 함께 인터페이스 식별자로 Symbol
또는 string
상수를 정의하는 것이 일반적입니다.
// interfaces.ts export interface ILogger { log(message: string): void; } export const TYPES = { Logger: Symbol.for("Logger"), };
2. 서비스 구현:
// services.ts import { injectable } from "inversify"; import { ILogger } from "./interfaces"; @injectable() export class ConsoleLogger implements ILogger { public log(message: string): void { console.log(`[ConsoleLogger] ${message}`); } } @injectable() export class FileLogger implements ILogger { public log(message: string): void { console.log(`[FileLogger] Saving to file: ${message}`); // 실제 앱에서는 파일에 기록합니다. } }
@injectable()
데코레이터를 주목하십시오. InversifyJS가 주입 대상으로 클래스를 표시합니다.
3. 로거를 사용하는 서비스 정의:
// app.ts import { injectable, inject } from "inversify"; import { ILogger, TYPES } from "./interfaces"; @injectable() export class Application { private _logger: ILogger; constructor(@inject(TYPES.Logger) logger: ILogger) { this._logger = logger; } public run(): void { this._logger.log("Application started!"); } }
여기서 @inject(TYPES.Logger)
는 InversifyJS에 ILogger
의 인스턴스를 생성자에 주입하도록 지시합니다.
4. IoC 컨테이너 구성:
// container.ts import { Container } from "inversify"; import { TYPES, ILogger } from "./interfaces"; import { ConsoleLogger, FileLogger } from "./services"; import { Application } from "./app"; const container = new Container(); // 로거 인터페이스를 해당 구현에 바인딩 container.bind<ILogger>(TYPES.Logger).to(ConsoleLogger); // 또는 .to(FileLogger) // Application 클래스 바인딩 container.bind<Application>(Application).toSelf(); // 클래스를 자체에 바인딩 export default container;
여기서 container.bind<ILogger>(TYPES.Logger).to(ConsoleLogger)
이 마법입니다. 컨테이너에 "TYPES.Logger
를 요청할 때마다 ConsoleLogger
의 인스턴스를 제공하라"고 알려줍니다.
5. 해결 및 실행:
// main.ts import container from "./container"; import { Application } from "./app"; // 컨테이너에서 Application의 인스턴스 가져오기 const app = container.get<Application>(Application); app.run(); // 출력: [ConsoleLogger] Application started!
범위
InversifyJS는 다른 바인딩 범위를 지원합니다.
.toConstantValue(value)
: 항상 동일한 사전 존재 값을 반환합니다..toDynamicValue(factory)
: 팩토리 함수에서 생성된 값을 반환합니다..toSelf()
: 클래스를 자체에 바인딩합니다 (서비스이면서 구현인 클래스에 유용)..inSingletonScope()
: 요청 시마다 동일한 인스턴스를 반환합니다..inTransientScope()
(기본값): 요청 시마다 새 인스턴스를 반환합니다.
싱글톤 예시:
container.bind<ILogger>(TYPES.Logger).to(ConsoleLogger).inSingletonScope();
container.ts
에서 ConsoleLogger
를 FileLogger
로 변경하면 Application
또는 main.ts
를 수정하지 않고 로깅 동작이 변경되어 IoC의 강력함을 보여줍니다.
TSyringe 심층 분석
TSyringe는 TypeScript 및 JavaScript를 위한 경량 종속성 주입 컨테이너로, 종속성을 관리하고 주입하는 간단한 방법을 제공합니다. Microsoft에서 개발했으며 InversifyJS보다 가벼운 대안으로 자주 간주되며 TypeScript의 실험적 데코레이터와 Reflect 메타데이터를 활용합니다.
핵심 원칙 및 설정
TSyringe는 InversifyJS와 매우 유사한 패턴을 따르며, 클래스를 장식하고 전역 컨테이너(또는 원하는 경우 특정 컨테이너)를 사용하여 종속성을 관리하고 주입하는 데 중점을 둡니다.
설치:
npm install tsyringe reflect-metadata npm install @types/reflect-metadata --save-dev
InversifyJS와 유사하게 tsconfig.json
에서 emitDecoratorMetadata
및 experimentalDecorators
를 활성화하고 애플리케이션 진입점에서 reflect-metadata
를 한 번 가져와야 합니다.
{ "compilerOptions": { "target": "es6", "module": "commonjs", "lib": ["es6"], "emitDecoratorMetadata": true, "experimentalDecorators": true, "sourceMap": true, // 디버깅 권장 "outDir": "./dist" } }
그리고 reflect-metadata
를 가져옵니다.
import "reflect-metadata";
예제 구현
TSyringe를 사용하여 로거 예제를 다시 구현해 보겠습니다.
1. 인터페이스/토큰 정의:
InversifyJS와 달리 Symbol
이 토큰으로 자주 사용되는 반면, TSyringe는 일반적으로 등록을 위해 문자열 또는 클래스(인터페이스를 나타내는 "토큰"으로 클래스)를 사용합니다. 인터페이스의 경우 InjectionToken
이 선호되는 방식입니다.
// interfaces.ts import { InjectionToken } from "tsyringe"; export interface ILogger { log(message: string): void; } export const ILoggerToken: InjectionToken<ILogger> = "ILogger";
2. 서비스 구현:
TSyringe는 InversifyJS와 마찬가지로 @injectable()
데코레이터를 사용합니다.
// services.ts import { injectable, registry } from "tsyringe"; import { ILogger, ILoggerToken } from "./interfaces"; @injectable() // 이 클래스를 ILoggerToken의 구현으로 자동 등록 // 간단한 경우 중앙 컨테이너의 명시적 바인딩 호출을 대체합니다. @registry([{ token: ILoggerToken, useClass: ConsoleLogger }]) export class ConsoleLogger implements ILogger { public log(message: string): void { console.log(`[ConsoleLogger (TSyringe)] ${message}`); } } @injectable() export class FileLogger implements ILogger { public log(message: string): void { console.log(`[FileLogger (TSyringe)] Saving to file: ${message}`); // 실제 앱에서는 파일에 기록합니다. } }
@registry
데코레이터를 주목하십시오. 이것은 클래스 정의 수준에서 구체적인 클래스를 인터페이스 토큰과 직접 연결하는 편리한 방법입니다. 이는 복잡한 애플리케이션의 경우 설정을 단순화할 수 있습니다. 또는 아래에서 볼 수 있듯이 container.register
를 수동으로 사용할 수 있습니다.
3. 로거를 사용하는 서비스 정의:
TSyringe는 InversifyJS와 마찬가지로 생성자 주입에 @inject()
를 사용합니다.
// app.ts import { injectable, inject } from "tsyringe"; import { ILogger, ILoggerToken } from "./interfaces"; @injectable() export class Application { private _logger: ILogger; constructor(@inject(ILoggerToken) logger: ILogger) { this._logger = logger; } public run(): void { this._logger.log("Application started!"); } }
4. IoC 컨테이너 구성 ( @registry
를 광범위하게 사용하지 않는 경우):
TSyringe는 가져와 사용할 수 있는 전역 컨테이너 container
를 제공합니다.
// main.ts (또는 전용 DI 설정 파일) import { container } from "tsyringe"; import { ILogger, ILoggerToken } from "./interfaces"; import { ConsoleLogger, FileLogger } from "./services"; import { Application } from "./app"; // 수동 등록 (ConsoleLogger의 @registry에 대한 대안) // container.register<ILogger>(ILoggerToken, { useClass: ConsoleLogger }); // FileLogger로 전환하려면: container.register<ILogger>(ILoggerToken, { useClass: FileLogger }); // Application의 경우, 직접 주입 가능하고 종속성을 취하므로 종속성이 해결되면 등록할 필요가 없을 수도 있습니다. // 그러나 나중에 명시적으로 확인하려면 등록할 수 있습니다. // container.register(Application, { useClass: Application }); // 또는 종속성이 충족되면 container.resolve(Application)가 직접 작동합니다. // 컨테이너에서 Application의 인스턴스 가져오기 const app = container.resolve(Application); // TSyringe는 생성자 종속성이 모두 해결된 경우 클래스를 직접 해결할 수 있습니다. app.run(); // 출력: [FileLogger (TSyringe)] Saving to file: Application started!
범위
TSyringe는 다른 수명 주기 범위도 지원합니다.
- 일시적 (기본값): 요청할 때마다 새 인스턴스가 생성됩니다.
container.register(ILoggerToken, { useClass: ConsoleLogger });
- 싱글톤: 요청할 때마다 동일한 인스턴스가 반환됩니다.
container.registerSingleton(ILoggerToken, ConsoleLogger); // 토큰 없이 클래스를 직접 싱글톤으로 등록하는 경우 container.registerSingleton(Application);
- 범위: 인스턴스는 특정 범위(예: 웹 요청) 내에서 공유됩니다. TSyringe는 이를 위해 자식 컨테이너를 만들 수 있습니다.
싱글톤 예시:
import { container } from "tsyringe"; import { ILogger, ILoggerToken } from "./interfaces"; import { ConsoleLogger } from "./services"; import { Application } from "./app"; container.registerSingleton<ILogger>(ILoggerToken, ConsoleLogger); const app = container.resolve(Application); app.run(); // 출력: [ConsoleLogger (TSyringe)] Application started!
InversifyJS와 TSyringe 중 선택
InversifyJS와 TSyringe 모두 TypeScript 애플리케이션에서 IoC를 구현하는 훌륭한 선택이지만, 결정에 영향을 미칠 수 있는 미묘한 차이가 있습니다.
- 성숙도 및 생태계: InversifyJS는 더 오래되었으며 더 큰 커뮤니티, 더 많은 예제 및 잠재적으로 더 많은 프레임워크와의 통합을 보유하고 있습니다. TSyringe는 Microsoft에서 지원하지만 광범위한 채택에서는 조금 더 새롭습니다.
- 경량 vs. 기능 풍부: TSyringe는 일반적으로 더 가볍고 간단한 DI의 경우 특히 간단한 API를 제공합니다. InversifyJS는 확장 가능한 바인딩 구문, 컨테이너 해결을 위한 미들웨어 및 바인딩 프로세스에 대한 더 세분화된 제어를 포함하여 더 풍부한 기능 세트를 제공합니다.
Symbol
vs.string
/class
토큰: InversifyJS는 문자열 리터럴 충돌을 피하기 위해 종종Symbol
을 토큰으로 권장하지만 문자열도 지원됩니다. TSyringe는 문자열 또는 클래스 참조를 토큰으로 사용하며, 이는 간단한 경우에 더 읽기 쉬울 수 있습니다.- 구성: TSyringe의
@registry
데코레이터를 사용하면 서비스에 대한 매우 간결한 인라인 등록이 가능하여 간단한 경우 대규모 중앙 구성 파일의 필요성을 줄일 수 있습니다. InversifyJS는 일반적으로 바인딩을Container
인스턴스 내에 중앙 집중화합니다. 대규모 애플리케이션의 경우 중앙 집중식 구성이 감독에 종종 유익합니다. - 오류 처리: 두 프레임워크 모두 해결할 수 없는 종속성에 대해 적절한 오류 메시지를 제공하지만 진단 기능은 약간 다를 수 있습니다.
InversifyJS를 선택해야 하는 경우:
- 고도로 구성 가능하고 확장 가능한 IoC 컨테이너가 필요합니다.
- 고급 기능(예: 사용자 지정 해결 후크, 특정 바인딩 구문 또는 자세한 디버그 정보)이 유용한 복잡한 애플리케이션을 구축하고 있습니다.
- 명시적이고 중앙 집중식 바인딩 구성을 선호합니다.
TSyringe를 선택해야 하는 경우:
- 단순성과 경량 솔루션을 우선시합니다.
@registry
를 통한 인라인 등록의 편리함을 높이 평가합니다.- Microsoft에서 지원하는 라이브러리가 추가적인 확신을 제공하는 환경에서 작업하고 있습니다.
- DI 요구 사항은 주로 생성자 주입 및 간단한 범위 지정입니다.
궁극적으로 두 프레임워크 모두 IoC 및 Dependency Injection를 효과적으로 구현하는 동일한 목표를 달성합니다. 선택은 종종 개인 선호도, 프로젝트 요구 사항 및 해당 API에 대한 친숙도에 따라 달라집니다.
결론
Dependency Injection을 사용한 Inversion of Control 구현은 강력하고 유지보수 가능하며 테스트 가능한 TypeScript 애플리케이션을 구축하는 초석입니다. 구성 요소를 분리하고 종속성 관리를 외부화함으로써 아키텍처 유연성의 새로운 수준을 활용할 수 있습니다. InversifyJS와 TSyringe는 각각 고유한 강점과 뉘앙스를 가지고 이러한 목표를 달성하기 위한 강력한 데코레이터 기반 메커니즘을 제공합니다. InversifyJS의 풍부한 기능 확장성을 선택하든 TSyringe의 간소화된 단순성을 선택하든, TypeScript 프로젝트에서 IoC 컨테이너를 채택하면 의심할 여지 없이 더 깔끔한 코드와 더 즐거운 개발 경험을 얻을 수 있습니다. 구현을 쉽게 교체하고 개별 코드 단위를 엄격하게 테스트할 수 있는 능력은 이러한 프레임워크가 지속적으로 제공하는 귀중한 이점입니다.