앞선 글에서 provider를 주입받는 방식은 Constructor-based 방식이었다.
즉, 생성자에서 멤버로 선언함과 동시에 의존성을 주입하는 것이었다.
일반적인 provider
일반적인 provider(@Injectable 로 데코레이팅된)를 등록할 때,
@Module안에서 providers 배열 안에 의존성을 주입하고자 하는 클래스들을 넣어준다.
// user.module.ts
import { module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { EmailService } from './email/email.service';
@Module({
controllers: [UserController],
providers: [UserService, EmailService]
})
export class UserModule;
보다시피 배열 안에 그냥 클래스 이름을 넣어주면 그만이지만 사실 다음의 코드를 축약한 것이라 보면 된다.
// user.module.ts
import { module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { EmailService } from './email/email.service';
@Module({
controllers: [UserController],
providers: [
{
provide: UserService,
useClass: UserService,
},
{
provide: EmailService,
useClass: EmailService,
},
]
})
export class UserModule;
provide에 넣어주는 값을 Injection Token이라 한다. 즉, 다른 곳에서 해당 provider를 사용하기 위해 주입할 때 쓰는 이름이다.
useClass는 실제로 주입받을 클래스 타입이다. 요컨대 위의 코드로 설명해 보자면, UserModule 내에서 'UserService'의 의존성을 주입할 때 UserService 클래스의 (싱글턴)인스턴스를 주입한다는 뜻이 된다.
custom provider
custom provider라고 하면 뭐 어려운 개념일 것 같은데, 그렇지만도 않다.
service, component, factory method, constant(상수) 등을 만들고 다른 곳에서 주입할 수 있게끔 @Injectable로 지정해 준다거나 다른 곳에서 @Inject 등으로 주입하면 그게 그냥 custom provider인 것이다.
의존성 주입을 위한 provider options
위에서 @Module안에 providers 배열에 넣어주는 클래스의 이름을 넣어주는 것은
provide와 useClass로 제공하는 값이 같은 경우임을 볼 수 있었다.
@Module에서 의존성을 주입할 때 useValue, useClass, useExisting 와 같은 value provider를 쓸 수 있다.
useValue
useValue를 사용하는 예시는 다음과 같은 것들이 있는데,
요컨데 상수와 같이 특정 값을 provider로써 사용하기 위해 useValue로 해당 값을 등록한다.
특히 단위 테스트할 때 요긴하게 쓰인다. 예를 들어,
CommonService가 다음과 같이 UserService를 주입해서 사용하고 있고,
// common.service.ts
import { Injectable } from '@nestjs/common';
import { UserService } from 'src/user/user.service';
@Injectable()
export class CommonService {
constructor(private readonly userService: UserService) {}
...
}
CommonService를 테스트하려고 할 때 실제 UserService의 인스턴스를 사용하는 것이 아닌
mocking된 객체를 대신 사용할 수 있다.
// common.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { CommonService } from './common.service';
import { UserService } from 'src/user/user.service';
const MockedUserService = { /** mocking implementation */ };
describe('CommonService', () => {
let service: CommonService;
let userService: UserService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
CommonService,
{
provide: UserService,
useValue: MockedUserService,
},
],
}).compile();
service = module.get<CommonService>(CommonService);
userService = module.get<UserService>(UserService);
});
...
});
단, useValue로 제공하는 값은 provide하려는 것과 같은 인터페이스를 가져야 한다.
useClass
일반적으로 많이 사용하는 방식일 것이다. 클래스를 provider로 제공하기 위해 사용한다.
얼핏보면 useValue와 비슷해 보이나, useValue는 상수는 일반 객체든 클래스든 주입이 가능하지만 useClass는 클래스 타입이어야 한다.
바로 위에서 든 예시로 든 common.spect.ts 에서 useValue를 useClass로 바꾸면 에러가 난다. MockedUserService가 클래스 타입이 아니기 때문이다.
useExisting
이미 존재하는 provider에 다른 별칭(alias)를 붙이기 위해 활용한다. 같은 provider에 대해 해당 이름으로 주입할 수도 있고 내가 설정한 alias로도 주입이 가능한 것이다.
단, alias를 지정한 provider를 등록하게 된다면 실제 provider도 함께 등록해두어야 한다.
가령, 다음과 같이 CommonModule에서 CommonService에 대한 별칭을 따로 설정해두었다고 할 때
// common.module.ts
import { Module } from '@nestjs/common';
import { CommonService } from './common.service';
import { CommonController } from './common.controller';
const CoService = {
provide: 'CoService',
useExisting: CommonService,
};
@Module({
imports: [],
providers: [CoService, CommonService],
controllers: [CommonController],
})
export class CommonModule {}
여기서 providers에 CoService만 넣고 CommonService를 넣지 않으면

Nest가 CoService에 대한 의존성을 찾을 수 없다고 에러를 띄우니 주의해야 한다.
저렇게 별칭을 설정해준 CoService라는 이름으로 주입받고 싶으면 다음과 같이 사용하면 될 것이다.
// common.controller.ts
import { Controller, Get, Inject, Injectable } from '@nestjs/common';
import { CommonService } from './common.service';
@Controller('common')
export class CommonController {
constructor(@Inject('CoService') private coService: CommonService) {}
@Get()
async example() {
return this.coService.getHello();
}
}
만약 저기서 CoService와 CommonService 둘 다 singleton 인스턴스를 사용하는 경우 같은 의존성을 가지게 된다.
useFactory
앞서 설명했던 Dynamic Module과 연관이 깊은데, useFactory는 factory 함수를 사용하여 동적으로 provider나 값을 구성한다.
Dynamic Module에서 TypeOrmModule.forRootAsync()에서 useFactory를 활용하여 환경변수를 읽어 데이터베이스 커넥션을 설정한 것이 바로 그 예이다.
가령 운영환경별로 환경변수를 다르게 읽어오는 ConfigService를 구성하고 싶다면 다음과 같은 예를 들어볼 수 있겠다.
import { Module } from '@nestjs/common';
import { ConfigService } from './config.service';
const configServiceProvider = {
provide: ConfigService,
useFactory: () => {
return new ConfigService(process.env.NODE_ENV);
},
};
@Module({
providers: [configServiceProvider],
exports: [configServiceProvider],
})
export class AppModule {}
상황에 따라 단순히 @Module 의 providers 배열 안에 클래스를 적어주는 게 아니라 위와 같이 다양한 방식으로 provider를 등록하는 일이 필요할 수 있다.
개인적으론 동적 모듈을 구성하기 위해 useFactory와 테스트를 위해 목 객체를 사용하기 위해 useValue를 사용하는 일이 많았던 것 같다.