Dependency Injection
Dependency Injection이란 IoC technique으로, instantiation of dependencies를 개발자가 코드에서 직접 수행하는 것이 아니라 IoC container에게 위임하는 것을 말한다.
우리가 Nest에서 provider를 정의할때, `@Injectable()` 데코레이터를 사용하게 된다.
import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';
@Injectable()
export class CatsService {
private readonly cats: Cat[] = [];
findAll(): Cat[] {
return this.cats;
}
}
이후 다음과 같이, controller class에서 이러한 Service class를 주입해달라고 요청할 수 있다.
import { Controller, Get } from '@nestjs/common';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';
@Controller('cats')
export class CatsController {
constructor(private catsService: CatsService) {}
@Get()
async findAll(): Promise<Cat[]> {
return this.catsService.findAll();
}
}
마지막으로, provider를 Nest의 IoC container에 등록한다.
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 {}
위 일련의 과정을 요약하면 다음과 같다.
1. `@injectable()` 데코레이터를 통해 CatsService가 Nest의 IoC container에 의해 관리됨을 선언한다.
2. controller에서는 생성자에 CatsService를 포함하면서 CatsService token에 대한 의존성을 선언한다.
3. `app.module.ts`에서 CatsService 토큰과 CatsService 클래스를 연관짓는다.
Nest IoC container가 CatsController를 instance화 하는 과정에서 해당 클래스에 존재하는 dependency를 확인한다.
이 과정에서 CatsService에 대한 dependency를 확인하게 되고, CatsService token에 대한 look up을 수행하며 CatsService class를 리턴받는다.
Standard providers
우리가 일반적으로 provider를 등록할 때는 아래와 같이 작성하게 된다.
@Module({
controllers: [CatsController],
providers: [CatsService],
})
그러나 실제로 `providers: [CatsService]` 에 해당하는 부분은 아래와 같은 의미를 가진다.
providers: [
{
provide: CatsService,
useClass: CatsService,
},
];
여기에서 `provide` 에 해당하는 부분은 IoC container에 등록할 토큰의 이름을, `useClass` 부분은 토큰을 통해 주입할 Class에 해당한다.
Custom providers
custom instance를 만들고 싶은 경우, 또는 testing 과정에서 class를 mock version으로 override하고 싶은 경우가 있을 수 있다.
이런 경우 custom provider를 사용한다.
Value providers: `useValue`
`useValue` syntax는 constant value를 주입할때 유용하게 사용할 수 있다. 외부 라이브러리를 Nest container에 집어 넣거나, 실제 구현을 mock object로 대체하고 싶은 경우 사용할 수 있다.
import { CatsService } from './cats.service';
const mockCatsService = {
/* mock implementation
...
*/
};
@Module({
imports: [CatsModule],
providers: [
{
provide: CatsService,
useValue: mockCatsService,
},
],
})
export class AppModule {}
여기서 mockCatsService는 CatsService와 동일한 인터페이스를 가진 literal object여야한다.
Non-class-based provider tokens
지금까지는 class의 이름을 그대로 provider token으로 사용하는 예시만을 살펴보았다.
이는 전형적인 constructor based injection에서 사용되는 패턴으로, 토큰이 그대로 클래스의 이름을 의미하는 경우이다.
다만 종종 DI token에 string값이나 symbol을 사용하고 싶을 수 있는데, 그때는 아래와 같이 사용할 수 있다.
import { connection } from './connection';
@Module({
providers: [
{
provide: 'CONNECTION',
useValue: connection,
},
],
})
export class AppModule {}
위 과정을 통해 우리는 string-valued token `'CONNECTION'`을 기존에 존재하는 connection object과 연관지을 수 있다.
이렇게 등록된 provider를 주입해주기 위해서는 `@Inject` 데코레이터를 사용한다.
@Injectable()
export class CatsRepository {
constructor(@Inject('CONNECTION') connection: Connection) {}
}