Nest.js의 대표적인 구성요소로는 Module
, Provider
, Service
가 있다. Nest.js Module
을 통해 외부 모듈을 import
하여 의존성 주입을 할 수 있으며, 반대로 모듈에 소속된 Provider를 외부로 export
할 수 도 있다. 그리고 모듈로서 선언을 하기 위해서는 모듈 클래스 위에 @Module
이라는 데커레이터가 존재한다. 우선 이 데커레이터의 메타데이터를 살펴보자.
import { Abstract } from '../abstract.interface';
import { Type } from '../type.interface';
import { DynamicModule } from './dynamic-module.interface';
import { ForwardReference } from './forward-reference.interface';
import { Provider } from './provider.interface';
/**
* Interface defining the property object that describes the module.
*
* @see [Modules](https://docs.nestjs.com/modules)
*
* @publicApi
*/
export interface ModuleMetadata {
/**
* Optional list of imported modules that export the providers which are
* required in this module.
*/
imports?: Array<
Type<any> | DynamicModule | Promise<DynamicModule> | ForwardReference
>;
/**
* Optional list of controllers defined in this module which have to be
* instantiated.
*/
controllers?: Type<any>[];
/**
* Optional list of providers that will be instantiated by the Nest injector
* and that may be shared at least across this module.
*/
providers?: Provider[];
/**
* Optional list of the subset of providers that are provided by this module
* and should be available in other modules which import this module.
*/
exports?: Array<
| DynamicModule
| Promise<DynamicModule>
| string
| symbol
| Provider
| ForwardReference
| Abstract<any>
| Function
>;
}
이중 살펴볼 것은 proivder
이다. Provider는 Module에서 배열 타입으로 선언되어있다. 즉 여러개의 Provider를 전달할 수 있다는 의미이다. provider
는 앱이 제공하고자 하는 핵심 기능 즉, 비즈니스 로직을 수행하는 역할을 한다. provider
에는 service,repository, factory, helper등 여러 형태로 구현이 가능ㅎ다.이를 통해 Single Point of Failure(단일 책임 원칙)을 방지할 수 있다.
기본적으로 nest g
를 통해 생성하는 service 또한 Provider지만, 필요에 따라 Custom Provider를 직접 작성해야하는 경우도 있다. Custom Provider가 필요한 경우는 아래의 경우들이 있다.
이번에는 Proider의 인터페이스를 살펴보자.
링크 : https://github.com/nestjs/nest/blob/master/packages/common/interfaces/modules/provider.interface.ts
import { Scope } from '../scope-options.interface';
import { Type } from '../type.interface';
import { InjectionToken } from './injection-token.interface';
import { OptionalFactoryDependency } from './optional-factory-dependency.interface';
export type Provider<T = any> =
| Type<any>
| ClassProvider<T>
| ValueProvider<T>
| FactoryProvider<T>
| ExistingProvider<T>;
export interface ClassProvider<T = any> {
/**
* Injection token
*/
provide: InjectionToken;
/**
* Type (class name) of provider (instance to be injected).
*/
useClass: Type<T>;
/**
* Optional enum defining lifetime of the provider that is injected.
*/
scope?: Scope;
/**
* This option is only available on factory providers!
*
* @see [Use factory](https://docs.nestjs.com/fundamentals/custom-providers#factory-providers-usefactory)
*/
inject?: never;
/**
* Flags provider as durable. This flag can be used in combination with custom context id
* factory strategy to construct lazy DI subtrees.
*
* This flag can be used only in conjunction with scope = Scope.REQUEST.
*/
durable?: boolean;
}
export interface ValueProvider<T = any> {
/**
* Injection token
*/
provide: InjectionToken;
/**
* Instance of a provider to be injected.
*/
useValue: T;
/**
* This option is only available on factory providers!
*
* @see [Use factory](https://docs.nestjs.com/fundamentals/custom-providers#factory-providers-usefactory)
*/
inject?: never;
}
export interface FactoryProvider<T = any> {
/**
* Injection token
*/
provide: InjectionToken;
/**
* Factory function that returns an instance of the provider to be injected.
*/
useFactory: (...args: any[]) => T | Promise<T>;
/**
* Optional list of providers to be injected into the context of the Factory function.
*/
inject?: Array<InjectionToken | OptionalFactoryDependency>;
/**
* Optional enum defining lifetime of the provider that is returned by the Factory function.
*/
scope?: Scope;
/**
* Flags provider as durable. This flag can be used in combination with custom context id
* factory strategy to construct lazy DI subtrees.
*
* This flag can be used only in conjunction with scope = Scope.REQUEST.
*/
durable?: boolean;
}
export interface ExistingProvider<T = any> {
/**
* Injection token
*/
provide: InjectionToken;
/**
* Provider to be aliased by the Injection token.
*/
useExisting: any;
}
Provider
는 결국 ClassProvider
, ValueProvider
, FactoryProvider
, ExistingProvider
와 Type
타입을 받을 수 있으며, 각각의 인터페이스가 정의되어있는것을 볼 수 있다. 그리고 각각이 무슨 역할 하는지를 살펴볼 것이다. Type
은 단순히 클래스 이름을 그대로 쓰는것을 의미한다. TypeScript에서 Class는 type 네임스페이스와 value 네임스페이스 각각에 생성된다는것을 되새긴다.
Value Provide는 provide
와 useValue
속성을 가진다. useValue는 어떤 타입도 받을 수 있다.
provide
는 injection token
이다. injection token
은 클래스 이름, 문자열, Symbol, Abstract, Function등을 사용할 수 있다.
useValue
구문을 사용하여 외부 라이브러리에서 프로바이더를 삽입하거나, 실제 구현을 모의객체로 대체할 수 있다.
예를 들어 아래와 같이 value provider를 구현했다고 가정하자
providers: [
{
provide: MailService,
useValue: mockedMailService
},
]
위와같이 작성하면, MailService
를 프로바이더로 지정하지만, 실제 값은 mockedMailService
로 사용(의존성 주입)하겠다는 의미가 된다.
예시로 문자열을 useValue
로 넘겨보도록 한다. inject token
은 문자열 CONNECTION
으로 지정한다.
// app.module.ts
@Module({
controllers: [AppController],
providers: [
AppService,
{
provide: 'CONNECTION',
useValue: 'value provider test',
}
],
})
export class AppModule {}
// app.service.ts
@Injectable()
export class AppService {
constructor(
@Inject('CONNECTION') private connection: string,
) {}
valueProvider(): string {
console.log(process.env.MODE);
return this.animal.sound();
}
}
// app.controller.ts
import { Controller, Get, Post } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('/value-provider')
public valueProvider(): string {
return this.appService.valueProvider();
}
}
이제 localhost:3000/value-provider
에 요청을 보내면 아래와 같이 value provider test
가 잘 반환된것을 볼 수 있다.
중요하게 봐야할것은 Module의 Value Provider 부분이다.
{
provide: 'CONNECTION',
useValue: 'value provider test',
}
그리고 이 provider를 주입시키기 위해 @Inject
parameter decorator를 사용하였다. @Inject
데코레이터는 injection token을 단일 매개변수로 받는다. 위 예시에서도 CONNECTION
이라는 문자열을 토큰으로 전달해 주입하는것을 볼 수 있다.
Class Provider는 useClass
속성을 사용한다. provide
속성은 동일하게 사용한다. Class Provider를 활용하여 프로바이더로 사용해야할 인스턴스를 동적으로 구성할 수 있다. 이 점을 활용하여 특정 조건에 따라 주입해야할 인스턴스를 달리 해줄 수 있다.
// animal.abstract.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export abstract class Animal {
public abstract sound(): string;
}
// cat.ts
import { Injectable } from '@nestjs/common';
import { Animal } from './animal.abstract';
@Injectable()
export class Cat extends Animal {
public sound(): string {
return 'meow';
}
}
// dog.ts
import { Injectable } from '@nestjs/common';
import { Animal } from './animal.abstract';
@Injectable()
export class Dog extends Animal {
public sound(): string {
return 'bark';
}
}
위와 같이 Animal
이라는 추상클래스와, 이를 구현한 Cat
,Dog
라는 클래스가 있다. 만약에 환경변수에 따라 production
인 경우에는 Dog
클래스를, 이외의 경우에는 Cat
클래스를 주입하고 싶다고 가정해본다.
// app.module.ts
@Module({
imports: [
LoggerModule.forRoot(AppService.name),
ConfigModule.forRoot({
isGlobal: true,
envFilePath: `${__dirname}/.env`,
}),
],
controllers: [AppController],
providers: [
AppService,
{
provide: Animal,
useClass: process.env.MODE === 'production' ? Dog : Cat,
},
],
})
export class AppModule {}
// app.service.ts
import { Inject, Injectable, Logger } from '@nestjs/common';
import { MyLogger } from './logger/logger.service';
import { Animal } from './class-provider-test/animal.abstract';
@Injectable()
export class AppService {
constructor(
private readonly animal: Animal,
) {}
classProvider(): string {
console.log(process.env.MODE);
return this.animal.sound();
}
}
// app.controller.ts
import { Controller, Get, Post } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('/class-provider')
public classProvider(): string {
return this.appService.classProvider();
}
}
injection token(provide
)를 Dog
, Cat
클래스의 상위 타입인 Animal
로 지정할 수 있다. 그리고 삼항연산자를 통해 클래스를 주입한다. 그리고 Animal
타입을 제공받은 Service는 Animal
추상 클래스에 선언된 추상메소드를 사용할 수 있다. .env
파일을 변경해보며 요청의 변화를 살펴보자.
그리고 위 예시에서 IoC
(Inversion Of Control, 제어반전)기술이 사용되는것을 볼 수 있다. Animal
객체의 관리는 IoC 컨테이너가 관리한다. 그리고 이를 구현한 클래스들을 useClass
속성에 필요에 따라 변경 및 분기문을 작성해 주면 되는것이다.
MODE="production"
MODE="dev"
팩토리 프로바이더 또한 인스턴스를 동적으로 구성하고자 할때 사용한다. 팩토리 프로바이더는 useFactory
를 사용한다. 앞서 봤던 useValue
, useClass
와 다른점은 함수로 되어있다는 것이다.
useFactory
은 원하는 인수와 리턴타입으로 작성하면 된다. 필요에 따라 useFactory
에서 다른 프로바이더를 주입받아야 한다면 inject
속성을 추가하여 전달해주면 된다.
{
provide: 'FACTORYTEST',
useFactory: (sf: SomeProvider) => {
// Factory Logic
}
inject: [SomeProvider]
},
Factory Provider를 활용해 Logger Dynamic Module을 만들어본다.이 글에서는 Dynamic Module에 대한 별도의 설명은 하지 않는다.(Dynamic Module에 대해)
기본적으로 NestJS에는 @nestjs/common
패키지에 Logger
클래스가 존재한다. 하지만, 내장 Logger는 콘솔에 기록만 할 수 있을뿐 파일로 저장하는 등 추가적인 작업을 하지 못한다. ConsoleLogger
클래스를 상속받아서 Logger 클래스를 확장시켜본다.
// logger.service.ts
import { ConsoleLogger } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { measureMemory } from 'vm';
@Injectable()
export class MyLogger extends ConsoleLogger {
constructor(classname: string) {
super(classname);
}
log(message: any, context?: string): void;
log(message: any, ...optionalParams: any[]): void;
log(message: unknown, context?: unknown, ...rest: unknown[]): void {
super.log.apply(this, [message]);
this.doSomething();
}
error(message: any, stackOrContext?: string): void;
error(message: any, stack?: string, context?: string): void;
error(message: any, ...optionalParams: any[]): void;
error(
message: unknown,
stack?: unknown,
context?: unknown,
...rest: unknown[]
): void {
super.error.apply(this, [message]);
this.doSomething();
}
private doSomething() {
console.log('Do something');
}
}
log
레벨과 error
레벨의 로그들을 출력하고, doSomething()
메소드를 추가적으로 실행한다고 가정한다.
다만 MyLogger
를 보면 생성자에 클래스 이름을 받는것을 볼 수 있다. 이는 Nest.js 로그에서 어떤 컨텍스트에서 발생한 로그인지를 판별해주는 부분을 출력값 역할을 한다
사용자가 LoggerModule
을 다른 모듈에 import
할때 forRoot
를 호출하여 컨텍스트 이름을 전달할 수 있도록 한다.
forRoot
는 Factory Provider를 통해 사용자가 전달한 이름을 MyLogger
에 전달하여 생성된 인스턴스를 반환하도록 한다. 여기서는 별도의 외부 프로바이더를 사용하지 않기 때문에 inject
속성을 사용하지 않는다.
.// logger.module.ts
@Module({})
export class LoggerModule {
static forRoot(serviceName: string): DynamicModule {
return {
module: LoggerModule,
providers: [
{
useFactory: () => {
return new MyLogger(serviceName);
},
provide: MyLogger,
},
],
exports: [MyLogger],
};
}
}
// app.module.ts
@Module({
imports: [
LoggerModule.forRoot(AppService.name),
ConfigModule.forRoot({
isGlobal: true,
envFilePath: `${__dirname}/.env`,
}),
],
controllers: [AppController],
})
export class AppModule {}
// app.service.ts
@Injectable()
export class AppService {
constructor(
private myLogger: MyLogger,
) {}
getHello(): string {
this.myLogger.log('Hello');
this.myLogger.error('world');
return 'Hello World!';
}
}
이제 요청을 보내면, log
,error
레벨 로그가 실행된 후 Do something
이 잘 출력되는것을 볼 수 있다.