Documentation | NestJS - A progressive Node.js framework
constructor(private catsService: CatsService) {}
Documentation | NestJS - A progressive Node.js framework
IoC는 어떤 과정을 거쳐 일어날까?
우리는 service에 @Injectable() 데코레이터를 붙여주고, controller의 생성자에서 해당 service를 인자로 적어주고, module.ts에 controller, service들을 명시해줌으로써 실제로는 다음과 같은 작업을 했다.
constructor(private catsService: CatsService)
이제 Nest IoC 컨테이너는 이런 과정을 거쳐간다.
위와 같은 과정을 dependency analysis라고 한다. 간략하게 설명했지만, 훨씬 복잡하고 application bootstrapping 동안에 일어난다고 한다. 하지만 큰 틀은, 위 과정은 transitive하고, bottom-up 방식이라는 것이다. CatsService가 의존성을 갖는다면, nest IoC 컨테이너는 이 의존성들을 먼저 생성하러 간다. 또한 이 의존성들이 올바른 순서로 resolve될 수 있도록 관리한다.
@Module({
controllers: [CatsController],
providers: [CatsService],
})
일반적으로 볼 수 있는 형태이다.
providers: [
{
provide: CatsService,
useClass: CatsService,
},
];
첫 예시에에서 providers 부분은 사실 이 코드의 짧은 버젼일 뿐이다.
provide 부분에 들어가는 것은 token, useClass는 말 그대로 사용할 class를 의미한다.
사실 각 인스턴스들은 provide 부분에 들어가는 token으로 식별되는 것이고, 그에 매핑되는 class를 위처럼 지정해야한다. 첫 예시의 경우 token과 class name을 동일하게 사용했을 뿐이다.
다음과 같은 상황에 유용하다고 말한다.
import { CatsService } from './cats.service';
const mockCatsService = {
/* mock implementation
...
*/
};
@Module({
imports: [CatsModule],
providers: [
{
provide: CatsService,
useValue: mockCatsService,
},
],
})
export class AppModule {}
실제로 쓰던 CatsService 대신 리터럴 오브젝트로 mockCatsService를 만들어 넣어줬다.
import { connection } from './connection';
@Module({
providers: [
{
provide: 'CONNECTION',
useValue: connection,
},
],
})
export class AppModule {}
token으로 꼭 class 이름을 쓸 필요 없다. 그냥 아무 string을 넣어도 된다.
CONNECTION이라는 string을 외부 파일에서 가져온 connection 객체에 매핑하는 예제이다.
이렇게 class based가 아닌 token을 쓸 때 @Inject() 데코레이터를 써서 의존성을 명시해야한다.
@Injectable()
export class CatsRepository {
constructor(@Inject('CONNECTION') connection: Connection) {}
}
이 방법은 token과 매핑되는 class를 동적으로 변하도록 만들어준다.
const configServiceProvider = {
provide: ConfigService,
useClass:
process.env.NODE_ENV === 'development'
? DevelopmentConfigService
: ProductionConfigService,
};
@Module({
providers: [configServiceProvider],
})
export class AppModule {}
providers 부분에 그냥 class이름을 적기도 했던 것을 기억해보자. 그 경우 해당 token과 class가 강제로 매핑된다. 하지만 위 예시와 같이 provide, useClass를 풀어서 쓰면, 특정 조건에 특정 class가 매핑되도록 할 수 있다.
provider를 찍어내는 함수에서 provider를 받아 쓰는 법이라고 생각할 수 있다.
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 {}
useFactory 키워드를 사용하며, 함수를 넣어준다. 해당 함수는 provider로 사용될 객체를 return해주면 된다.
함수는 다른 provider를 인자를 받을 수 있으며, inject 키워드 뒤에 배열로 명시해준다. 해당 의존성들은 useFactory에 매핑된 함수에 들어가기전에 resolve된다.
각 의존성들은 optional 표기를 통해 optional 여부를 표현할 수 있다. optional이 true라면, 없는 경우 undefined로 resolve된다.
@Injectable()
class LoggerService {
/* implementation details */
}
const loggerAliasProvider = {
provide: 'AliasedLoggerService',
useExisting: LoggerService,
};
@Module({
providers: [LoggerService, loggerAliasProvider],
})
export class AppModule {}
하나의 provider를 가리키는 2가지의 token을 만드는 법이다. LoggerService는 ‘AliasedLoggerService’와 LoggerService 클래스 이름 두가지 토큰으로 매핑된 것
어디다 쓸 수 있을지는 모르겠다.
provider들은 해당 provider가 선언된 module를 scope으로 갖는다. 해당 module이 nest IoC 컨테이너에 의해 읽힐 때 모두 resolve되는 것이다.
provider를 module 바깥에서도 인지하도록 하기 위해서는 export해야한다.
const connectionFactory = {
provide: 'CONNECTION',
useFactory: (optionsProvider: OptionsProvider) => {
const options = optionsProvider.get();
return new DatabaseConnection(options);
},
inject: [OptionsProvider],
};
@Module({
providers: [connectionFactory],
exports: [connectionFactory],
// exports: ['CONNECTION']
})
export class AppModule {}
token 혹은 provider 자체로 export할 수 있다.