Providers
Service, Repository, Factory, Helper 등등, 대부분의 Nest클래스는 프로바이더로 취급 가능하다.
프로바이더는 Nest에서 종속성 주입(Dependency Injection)이 가능한데, 이로 인해 Nest에서는 다양한 관계를 지닌 객체 인스턴스의 연결을 Nest의 런타임 시스템에 위임할 수 있다.
Nest에는 프로바이더 간의 관계를 담당하는 내장 IoC(Inversion of Control)컨테이너가 존재하는데
@Injectable 데코레이터를 사용하여 Nest IoC 컨테이너에서 관리할 수 있는 클래스임을 선언하는 메타데이터를 첨부할 수 있다.
Nest는 기본적으로 싱글톤 패턴을 따르는 프레임워크이다. Nest는 Bootstrap될 때 Module에 등록된 의존성들을 검사하고 프로바이더 인스턴스를 생성한다. 이는 애플리케이션 전체에서 단일 인스턴스로서 공유되며 이 덕에 Nest는 빠른 속도로 요청들을 처리할 수 있다.
기본적으로 Nest에서 인스턴스 수명은 애플리케이션과 동기화되며, 각 요청별 또는 호출 될 때마다 생성되고 Garbage-Collected되도록 따로 Scope를 지정할 수도 있다.
Example
// cats.service.ts
import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';
@Injectable()
export class CatsService {
private readonly cats: Cat[] = [];
findAll(): Cat[] {
return this.cats;
}
}
- @Injectable() 데코레이터가 CatsService 클래스를 프로바이더로 표시
// cats.controller.ts
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();
}
}
- Nest가 providers를 컨트롤러 클래스에 주입하도록 요청
- 이를 통해 CatController 안에서 별도의 작업없이도 this를 통해 catService의 호출이 가능해진다.
// cats.module.ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {}
- provider에 CatsService를 넣어 Nest IoC 컨테이너에 Provider로서 등록한다.
Custom Provider
프로바이더는 service가 아니더라도 모든 값을 제공할 수 있다.
이에 기반하여 표준 Provider에서 제공하는 것 이상의 기능이 필요할 경우 아래 등의 목적을 위해 Custom Provider를 제작할 수 있다.
- 사용자 지정 인스턴스를 만들고 싶을 때
- 두번째 종속성에서 기존 클래스를 재사용하려 할 때
- 테스트를 위해 모의(mock)버전으로 클래스를 재정의 할 때
축약 표기법과 명시적 표기법
Custom Provider를 사용하려면 먼저 Provider를 등록하는 명시적 방법을 알아야 한다.
@Module({
controllers: [CatsController],
providers: [CatsService],
})
위는 nest g 명령어를 사용하면 흔히 볼 수 있는 기본적인 형태(축약표기법)이다.
아래는 위 구문의 명시적 형태이다.
@Module({
controllers: [CatsController],
providers: [
{
provide: CatsService,
useClass: CatsService,
},
]
});
provide에 제공된 토큰과 useClass 에 제공된 값은 각각 일종의 key와 value가 되어 IoC Container에 등록되고 DI(Dependency Injection)을 진행할 수 있게 된다.
주요 사용
value provider : useValue
provide로 제공되는 key 값이 useValue에 제공되는 value값에 매칭된다.
사용목적은 아래와 같다.
- 상수값을 삽입 → provide에 들어갈 값을 costant.ts 등에서 미리 정의해두기도 함
- Nest컨테이너에 외부 라이브러리 삽입
- 실제 구현을 모의 객체로 대체 → 테스트
Example1
import { CatsService } from './cats.service';
const mockCatsService = {
/* mock implementation
...
*/
};
@Module({
imports: [CatsModule],
providers: [
{
provide: CatsService,
useValue: mockCatsService,
},
],
})
export class AppModule {}
위의 예제에서 CatsService 토큰은 mockCatsService로 resovle된다.
이는 TypeScript의 Type Compatibility 덕분에 호환되는 인터페이스가 있는 모든 객체를 사용 가능하기 때문이다.
Example2
import { connection } from './connection';
@Module({
providers: [{
provide: 'CONNECTION',
useValue: connection,
}, {
provide: 'MASTER_NAME',
useValue: 'KGH'
}],
})
export class AppModule {}
위와 같이 프로바이더를 정의했을 때 아래와 같은 방법들로 사용이 가능하다.
// DI 방식
@Injectable()
export class CatsRepository {
constructor(@Inject('CONNECTION') connection: Connection) { }
}
// 단순 삽입
@Injectable()
export class CatsService {
private readonly cats: Cat[] = [];
@Inject('MASTER_NAME') name: string;
}
Example3
// app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [
AppService,
{
provide: 'PORT',
useValue: 3000
}
],
})
export class AppModule {}
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const port = app.get('PORT')
console.log(port) // -> 3000
await app.listen(port);
}
bootstrap();
위와 같이 등록된 프로바이더를 main.ts에서 app.get을 통해 접근할 수도 있다.
value provider : useClass
객체(프로바이더, 가드 등)를 재정의할 인스턴스를 제공하기 위해 인스턴스화될 클래스를 제공한다.
토큰이 확인하는 클래스를 동적으로 결정하며 다음과 같이 쓰일 수 있다.
- 객체(프로바이더, 가드 등)를 재정의
- 변수나 개발환경에 따른 제공 인스턴스 변경
Example1
const configServiceProvider = {
provide: ConfigService,
useClass:
process.env.NODE_ENV === 'development'
? DevelopmentConfigService
: ProductionConfigService,
};
@Module({
providers: [configServiceProvider],
})
export class AppModule {}
위와 같이 애플리케이션이 개발버전인지, 배포버전인지에 따라 다른 Class를 사용할 수 있다.
Example2
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_GUARD,
useClass: RolesGuard,
},
],
})
export class AppModule {}
위의 예제는 모듈 외부에서 선언된 RolesGuard 객체를 의존성 주입하기 위해 사용
의 예제는 Nest는 위와 같이 Filter, Guard, Interceptor, pipe를 위한 상수를 제공한다.
value provider : useFactory
객체(프로바이더, 가드 등)를 재정의할 인스턴스를 제공하기 위해 인스턴스화될 클래스를 제공한다.
Factory라는 이름에서 보이듯, 동적으로 Provider를 생성할 수 있으며, 인수를 받고 함수로 값을 만드는 것 또한 가능하다. 또한 async/await을 사용하여 database연결 등의 비동기 작업에 대한 DI가 가능하다.
Example1
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
type: 'mysql',
host: configService.get('HOST'),
port: configService.get<number>('PORT'),
username: configService.get('USERNAME'),
password: configService.get('PASSWORD'),
database: configService.get('DATABASE'),
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
}),
inject: [ConfigService],
});
인수를 받아 동적으로 인스턴스를 생성하는 예제이다.
Example2
{
provide: 'ASYNC_CONNECTION',
useFactory: async () => {
const connection = await createConnection(options);
return connection;
},
}
Provider 파트는 DI개념을 처음 접했던 나로서는 Nest를 공부하면서 뭔가 100% 이해하기가 힘든 부분이였다.
하지만 프로바이더를 공부하며 SOLID원칙, DI, IoC등 Express로 개발하며 놓쳤던 개념들에 대해 공부할 수 있어서 의미 깊은 시간이였던 것 같다.(이런 시간을 갖게 해준 Mash-UP에게 Shout Out)
프로바이더와 DI와 관련된 내용은 Nest에 있어서도 매우 핵심적인 내용인 듯 하니 이를 아름답게 사용하기 위해선 꾸준한 공부와 연습이 필요할 것 같다.
Reference
'Node.js > Nest.js' 카테고리의 다른 글
NestJS - Decorator를 활용한 Discord봇 개발환경 개선 (0) | 2023.03.16 |
---|---|
[TypeORM] Active Recore & Data Mapper Pattern (0) | 2021.08.30 |
NestJS Lazy-loading Modules (0) | 2021.08.08 |