지금까지는 constructor 기반의 DI를 이용했다.
어플리케이션이 복잡해지면, DI 시스템의 다른 기능이 필요할 수 있음
긍까 한번 알아보자
DI(의존성 주입)은 의존성의 인스턴스화를 IoC 컨테이너(우리의 경우 NestJS 런타임 시스템)에 위임하는 inversion of control(IoC) 기술이다. 요거에 대해 자세히 알아보자.
먼저, 프로바이더를 정의하자. CatsService
클래스에 @Injectable()
데코레이터를 달아서 프로바이더라고 알린다.
import { Injectable } from '@nestjs/common';
@Injectable()
export class CatsService {}
다음으로 컨트롤러에 프로바이더를 주입하라고 Nest에게 요청한다.
import { Controller } from '@nestjs/common';
import { CatsService } from './cats.service';
@Controller('cats')
export class CatsController {
constructor(private catsService: CatsService) {}
}
마지막으로, Nest IoC 컨테이너에 프로바이더를 등록한다.
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
import { CatsService } from './cats/cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class AppModule {}
이게 어떻게 가능한 것일까? 3가지 단계로 나눌 수 있다.
cats.service.ts
에서 @Injectable()
데코레이터가 CatsService
클래스를 Nest IoC 컨테이너에 의해 관리되는 클래스로 선언한다.cats.controller.ts
에서 CatsController
는 생성자 주입을 통해 CatsService
토큰에 대한 종속성을 선언한다.constructor(private catsService: CatsService)
app.module.ts
에서 CatsService
토큰을 cats.service.ts
파일의 CatsService
와 연결(등록)한다.그렇다면 연결은 어떻게 발생할까?
Nest IoC 컨테이너가 CatsController
를 인스턴스화할 때, 모든 의존성을 찾는다. CatsService
의존성을 발견하면 위의 연결 3단계에 따라 CatsService
클래스를 반환하는 CatsService
토큰을 찾는다. 기본 동작인 싱글톤 스코프라고 가정하면, Nest는 CatsService
의 인스턴스를 만들고 캐시한 후 반환하거나 이미 캐시된 경우 기존 인스턴스를 반환한다.
@Module({
controllers: [CatsController],
providers: [CatsService],
})
이거는 사실 단순화된 것이고, 아래가 표준이다.
providers: [
{
provide: CatsService,
useClass: CatsService,
},
];
코드를 보면 CatsService
토큰을 CatsService
클래스와 연결하는 것을 확인할 수 있다.
Nest는 이런 경우들을 위해 커스텀 프로바이더 정의를 허용한다.
HINT
의존성 해결에 문제가 있다면NEST_DEBUG
환경변수를 설정해서 로그를 확인해보자
useValue
useValue
구문은 상수 값을 주입하거나 외부 라이브러리를 Nest 컨테이너에 넣거나, 실제 구현을 mock 개체(모의 개체)로 바꾸는 데 유용하다. 테스트 목적으로 모의 CatsService
를 사용하도록 해보자.
import { CatsService } from './cats.service';
const mockCatsService = {
/* mock implementation
...
*/
};
@Module({
imports: [CatsModule],
providers: [
{
provide: CatsService,
useValue: mockCatsService,
},
],
})
export class AppModule {}
이 예제에서, CatsService
토큰은 mockCatsService
모의 개체로 리졸브한다. useValue
는 CatsService
와 동일한 인터페이스인 리터럴 개체를 요구한다. TypeScriptdml 구조 타이핑으로 인해 리터럴 개체나 클래스 인스턴스를 포함하여 호환되는 인스페이스가 있는 모든 개체를 사용할 수 있다.
지금까지 우리는 클래스 이름을 프로바이더 토큰으로 사용했다. 이것은 생성자 기반 주입에 사용되는 표준 패턴과 일치한다. DI 토큰으로 문자열이나 심볼을 사용할 수도 있다.
import { connection } from './connection';
@Module({
providers: [
{
provide: 'CONNECTION',
useValue: connection,
},
],
})
export class AppModule {}
문자열 토큰인 'CONNECTION'
과 connection
개체를 연결했다.
NOTICE
문자열을 사용하는 대신, 자바스크립트의 심볼이나 타입스크립트의 enums 사용 가능
문자열 토큰을 사용하는 프로바이더의 주입은 @Inject()
데코레이터를 사용한다.
@Injectable()
export class CatsRepository {
constructor(@Inject('CONNECTION') connection: Connection) {}
}
위 예에서는 문자열을 그대로 사용했지만 constants.ts
같은 파일에 토큰을 정의해서 사용하는 것이 좋다.
useClass
useClass
구문은 리졸브할 클래스를 동적으로 결정할 수 있게 한다. 예를 들어, 추상의 ConfigService
가 있다고 하자. 현재 환경에 따라 다른 config service를 구현해보자.
const configServiceProvider = {
provide: ConfigService,
useClass: process.env.NODE_ENV === 'development'
? DevelopmentConfigService
: ProductionConfigService,
};
@Module({
providers: [configServiceProvider],
})
export class AppModule {}
configServiceProvider
를 리터럴 객체로 정의한 다음 모듈 데코레이터의 프로바이더 속성에 전달한다.ConfigService
클래스 이름을 토큰으로 사용했다. ConfigService
에 의존하는 모든 클래스에 대해 DevelopmentConfigService
또는 ProductionConfigService
의 인스턴스를 주입하여 다른 곳에서 선언된 기본 구현(예: @Injectable()
데코레이터로 선언된 ConfigService
)을 재정의한다.useFactory
useFactory
구문을 사용하면 프로바이더를 동적으로 생성할 수 있다. 실제 프로바이더는 팩토리 함수에서 반환된 값으로 공급된다. 팩토리 함수는 단순하거나 복잡할 수 있는데, 복잡한 팩토리는 결과를 계산하는 데 필요한 다른 프로바이더를 주입할 수 있다. 후자의 경우, 팩토리 프로바이드 구문에는 다음과 같은 관련 메커니즘이 있다.
팩토리 함수는 인수를 허용할 수 있다.
inject 속성(옵셔널)은 Nest가 확인하며 인스턴스화 과정 중에 팩토리 함수에 인수로 전달할 공급자의 배열을 받는다.
const connectionProvider = {
provide: 'CONNECTION',
useFactory: (optionsProvider: OptionsProvider, optionalProvider?: string) => {
const options = optionsProvider.get();
return new DatabaseConnection(options);
},
inject: [OptionsProvider, { token: 'SomeOptionalProvider', optional: true }],
// \_____________/ \__________________/
// This provider The provider with this
// is mandatory. token can resolve to `undefined`.
};
@Module({
providers: [
connectionProvider,
OptionsProvider,
// { provide: 'SomeOptionalProvider', useValue: 'anything' },
],
})
export class AppModule {}
useExisting
useExisting
구문은 기존 프로바이더에 대한 별칭을 만들 수 있다.
@Injectable()
class LoggerService {
/* implementation details */
}
const loggerAliasProvider = {
provide: 'AliasedLoggerService',
useExisting: LoggerService,
};
@Module({
providers: [LoggerService, loggerAliasProvider],
})
export class AppModule {}
이렇게 하면 LoggerService
는 AliasedLoggerService
로도 액세스할 수 있다.
프로바이더는 어떤 값이든 제공할 수 있다. 예를 들어, 프로바이더가 현재 환경에 따라 configuration 객채 배열을 제공하는 코드를 작성해보자.
const configFactory = {
provide: 'CONFIG',
useFactory: () => {
return process.env.NODE_ENV === 'development' ? devConfig : prodConfig;
},
};
@Module({
providers: [configFactory],
})
export class AppModule {}
다른 프로바이더와 같이, 커스텀 프로바이더는 선언된 모듈 범위이다. 다른 모듈에서 사용하려면 export되어야 한다. 커스텀 프로바이더를 export하기 위해서 토큰이나 프로바이더 객체 자체를 이용할 수 있다.
// 토큰 이용
const connectionFactory = {
provide: 'CONNECTION',
useFactory: (optionsProvider: OptionsProvider) => {
const options = optionsProvider.get();
return new DatabaseConnection(options);
},
inject: [OptionsProvider],
};
@Module({
providers: [connectionFactory],
exports: ['CONNECTION'],
})
export class AppModule {}
// 프로바이더 객체 이용
const connectionFactory = {
provide: 'CONNECTION',
useFactory: (optionsProvider: OptionsProvider) => {
const options = optionsProvider.get();
return new DatabaseConnection(options);
},
inject: [OptionsProvider],
};
@Module({
providers: [connectionFactory],
exports: [connectionFactory],
})
export class AppModule {}