NestJS 외 의존성 주입 - tsyringe 및 InversifyJS 심층 분석
Wenhao Wang
Dev Intern · Leapcell

분리의 힘: TypeScript 프로젝트에서의 의존성 주입
현대 소프트웨어 개발의 발전하는 환경에서 확장 가능하고 유지보수 가능하며 테스트 가능한 애플리케이션을 구축하는 것이 가장 중요합니다. 이러한 목표를 달성하는 데 크게 기여하는 아키텍처 패턴 중 하나는 의존성 주입(DI)입니다. NestJS와 같은 프레임워크는 DI를 완벽하게 통합하지만, 이 생태계 외부의 많은 TypeScript 프로젝트도 그 원칙으로부터 큰 이점을 얻을 수 있습니다. 이 글에서는 TypeScript의 두 가지 주요 DI 컨테이너인 tsyringe
와 InversifyJS
가 독립적인 프로젝트를 어떻게 강화하고 모듈성을 촉진하며 강한 결합을 줄이는지 살펴봅니다. 핵심 메커니즘, 구현 세부 정보 및 실제 사용 사례를 탐색하여 요구 사항에 맞는 올바른 도구를 선택하는 데 도움을 드릴 것입니다.
핵심 개념 이해하기
tsyringe
와 InversifyJS
의 특정 내용으로 들어가기 전에 몇 가지 기본적인 DI 개념에 대한 이해를 확실히 해봅시다.
- 의존성 주입 (DI): 컴포넌트가 내부적으로 생성하는 대신 외부 소스에서 종속성을 받는 디자인 패턴입니다. 이 "제어 역전"은 느슨한 결합을 촉진하여 컴포넌트를 더 쉽게 테스트, 재사용 및 유지보수할 수 있도록 합니다.
- IoC 컨테이너 (제어 역전 컨테이너): DI 컨테이너라고도 하며, 객체의 인스턴스화 및 수명 주기와 그 종속성을 관리하는 프레임워크 또는 라이브러리입니다. 구성 또는 데코레이터를 기반으로 하는 컴포넌트의 "배선"을 처리합니다.
- 바인딩/등록: 요청 시 특정 종속성의 인스턴스를 제공하는 방법을 DI 컨테이너에 알리는 프로세스입니다. 이는 종종 인터페이스 또는 추상 클래스를 구체적인 구현에 매핑하는 것을 포함합니다.
- 해결: DI 컨테이너에서 종속성의 인스턴스를 요청하는 작업입니다. 컨테이너는 바인딩을 찾고 필요한 컴포넌트를 인스턴스화하여 주입합니다.
- 데코레이터: 클래스, 메서드, 속성 또는 매개변수에 첨부할 수 있는 특수 유형의 선언입니다. DI의 맥락에서 데코레이터는 종종 클래스를 주입 가능으로 표시하거나, 주입 지점을 정의하거나, 바인딩을 구성하는 데 사용됩니다.
- 서비스 식별자: IoC 컨테이너 내에서 특정 종속성을 식별하는 데 사용되는 고유한 키(종종 문자열, 심볼 또는 클래스 생성자)입니다.
tsyringe: 경량의 데코레이터 기반 접근 방식
Microsoft에서 개발한 tsyringe
는 TypeScript용 경량의 성능이 뛰어난 의존성 주입 컨테이너입니다. TypeScript의 실험적 데코레이터 및 리플렉션 기능을 활용하여 종속성 관리를 대폭 단순화합니다. API는 직관적이며 "TypeScript 네이티브" 느낌을 줍니다.
구현 및 적용
실용적인 예제, 즉 간단한 알림 서비스를 통해 tsyringe
를 설명해 보겠습니다.
먼저 tsyringe
를 설치합니다:
npm install tsyringe reflect-metadata
그리고 tsconfig.json
에서 emitDecoratorMetadata
와 experimentalDecorators
가 활성화되어 있는지 확인합니다:
{ "compilerOptions": { "emitDecoratorMetadata": true, "experimentalDecorators": true, // ...other options } }
이제 서비스를 정의해 보겠습니다:
// services/notifier.ts import { injectable } from "tsyringe"; interface INotifier { send(message: string): void; } @injectable() class EmailNotifier implements INotifier { send(message: string): void { console.log(`Sending email: ${message}`); } } @injectable() class SMSNotifier implements INotifier { send(message: string): void { console.log(`Sending SMS: ${message}`); } } export { INotifier, EmailNotifier, SMSNotifier };
다음으로, 알림 서비스를 사용하는 서비스입니다:
// services/user-service.ts import { injectable, inject } from "tsyringe"; import { INotifier } from "./notifier"; @injectable() class UserService { constructor(@inject("INotifier") private notifier: INotifier) {} registerUser(username: string): void { console.log(`User ${username} registered.`); this.notifier.send(`Welcome, ${username}!`); } } export { UserService };
마지막으로 애플리케이션을 부트스트랩하고 종속성을 해결합니다:
// app.ts import "reflect-metadata"; // Must be imported once at the top of your entry file import { container } from "tsyringe"; import { INotifier, EmailNotifier, SMSNotifier } from "./services/notifier"; import { UserService } from "./services/user-service"; // Registering dependencies // We can bind an interface to a concrete implementation container.register<INotifier>("INotifier", { useClass: EmailNotifier }); // Or we can register by class type directly if no interface is used or for multiple implementations // container.register(EmailNotifier); // container.register(SMSNotifier); // At this point, if we wanted to change the notifier, we'd just change the registration above // For example, to use SMSNotifier: // container.register<INotifier>("INotifier", { useClass: SMSNotifier }); // Resolving the UserService (which transitively resolves INotifier) const userService = container.resolve(UserService); userService.registerUser("Alice"); // We can also have transient services (new instance each time) vs. singletons (one instance) // By default, tsyringe classes are transient unless specified. // To make EmailNotifier a singleton: // container.registerSingleton<INotifier>("INotifier", EmailNotifier);
이 예제에서 @injectable()
은 클래스를 주입 대상으로 표시합니다. @inject("INotifier")
는 tsyringe
에게 `