본문 바로가기

Backend/NestJS

[NestJS] NestJS 공식 문서 정독(1)(Custom Provider,Async Provider)

728x90
NestJS의 Fundamentals 부분을 정독해 보려고 한다.
Custom Provider에 대한 첫 번째 정독 기록이다.

개요

DI(Dependency Injection)
지금까지는 의존성 주입으로 거의 생성자 기반 주입만을 다뤄봤다.

어플리케이션이 복잡해지면,

NestJS의 DI 시스템 전체 기능을 이해해야 한다.

 

여러 가지의 provider 생성 방법과 의존성 주입의 방법에 대해 알아보자.

DI Fundamentals

DI는 IOC 기술로 종속성의 프로바이더의 인스턴스화를 직접 수행하는 대신

IOC 컨테이너에 위임함으로써 구현한다.

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();
  }
}

 CatService라는 프로바이더가 있다고 할 때

Controller에서 해당 프로바이더를 주입해야 한다고 해보자.

 

CatsService는 우리가 잘 알듯이 Injectable 데커레이터로 선언된 프로바이더이다.

CatsController는 생성자 주입을 통해 CatsService 토큰에 대한 종속성을 선언한다.

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 {}

위의 코드에서 AppModule은

CatsService 토큰을 cats.service.ts 파일의 CatsService 클래스와 연결한다.

 

Nest IOC 컨테이너가 CatController를 할 때 먼저 종속성을 찾는다.

그러면 CatService라는 종속성을 발견하게 되고,

실제 CatsService 클래스와 연결된 CatsService 토큰을 조회한다.

 

이후에는 CatsService의 인스턴스를 생성하고 캐싱 후 반환하거나

이미 생성되어 캐싱했다면 기존 인스턴스를 반환한다.(SINGLETON)

 

Standard Providers

토큰 개념을 도입해 본래 프로바이더 사용법을 보자.

providers: [
  {
    provide: CatsService,
    useClass: CatsService,
  },
];

실제로는 위와 같이 provide에 토큰, useClass에 실제 클래스 이름을 전달한다.

 

지금까지 사용하던 short-hand notation은

토큰이 실제 클래스 이름과 동일한 이름으로 사용된 편의를 위한 표기법이다.

 

Custom Provider

일반적인 프로바이더로는 구현하기 힘든 기능이 있다.

바로 아래와 같은 상황들이다.

  • 사용자 정의 인스턴스를 생성하고 싶을 때(config에 따라 다른)
  • 테스트를 위한 모의 버전으로 클래스를 재정의하고 싶을 때
  • 두 번째 종속성에서 기존 클래스를 재사용하고 싶을 때

이럴 때는 프로바이더를 직접 만들어야 한다. 

useValue

useValue는 상수 값을 주입하거나 외부 라이브러리를 Nest 컨테이너에 넣거나 

실제 구현을 위한 모의 객체로 대체하는 데 유용하다.

import { CatsService } from './cats.service';

const mockCatsService = {
  /* mock implementation
  ...
  */
};

@Module({
  imports: [CatsModule],
  providers: [
    {
      provide: CatsService,
      useValue: mockCatsService,
    },
  ],
})
export class AppModule {}

위의 코드에서는 CatsService 토큰이 mockCatsService 모의 객체로 연결된다.

useValue에 들어갈 수 있는 값은 아래와 같다.

  • 토큰(CatsService)과 동일한 인스턴스를 가지는 리터럴 객체
  • 토큰(CatsService)과 호환이 가능한 인스턴스를 가진 객체(new를 이용한 클래스 인스턴스 포함)

Non-class-based provider token

지금까지는 프로바이더의 토큰으로 클래스 이름으로만 사용했다.

하지만 문자열이나 기호 또한 DI 토큰으로 사용할 수 있다.

import { connection } from './connection';

@Module({
  providers: [
    {
      provide: 'CONNECTION',
      useValue: connection,
    },
  ],
})
export class AppModule {}

위와 같이 문자열도 이미 존재하는 객체에 대한 토큰으로 이용될 수 있다.

 

그렇다면 위처럼 설정한 프로바이더는 어떻게 주입될까?

@Injectable()
export class CatsRepository {
  constructor(@Inject('CONNECTION') connection: Connection) {}
}

위처럼 Inject 데커레이터의 인자로 토큰으로 사용한 문자열을 전달하면

connection 객체를 사용할 수 있다.

토큰을 문자열로 사용하는 경우
해당 문자열은 constants.ts와 같은 분리된 파일에서 한 번에 관리하는 것이 좋다.

useClass

useClass 구문은 토큰과 연결되는 클래스를 동적으로 결정할 수 있다.

const configServiceProvider = {
  provide: ConfigService,
  useClass:
    process.env.NODE_ENV === 'development'
      ? DevelopmentConfigService
      : ProductionConfigService,
};

@Module({
  providers: [configServiceProvider],
})
export class AppModule {}

위의 예시에서는 현재 환경에 따라 ConfigService 토큰과 연결되는 클래스가 달라진다.

 

위의 예시에서는 리터럴 객체로 configServiceProvider를 리터럴 객체로 정의하고,

Module 데커레이터에 전달한다.

 

ConfigService 클래스 이름을 토큰으로 사용했다.

ConfigService에 의존하는 모든 클래스는 현재 환경에 따라 다른 클래스의 인스턴스를 주입받는다.

useFactory

useFactory는 동적인 프로바이더 생성이 가능하다.

실제 프로바이더는 팩터리 함수에서 반환된 값으로 제공된다.

 

다른 프로바이더에 의존하지 않는 단순 팩터리가 있을 수 있고,

결과를 반환하는 데 다른 공급자에 의존적인 복잡한 팩터리 있을 수 있다.

 

때문에 선택적으로 inject 속성을 사용할 수 있다.

inject 속성의 값은 팩터리 함수의 인자로 전달되는 프로바이더 배열이다.

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 {}

Alias providers

useExisting을 사용하면 기존 프로바이더의 Alias를 만들 수 있다.

따라서 동일한 프로바이더에 액세스 하는 방법이 두 개가 되는 것이다.

@Injectable()
class LoggerService {
  /* implementation details */
}

const loggerAliasProvider = {
  provide: 'AliasedLoggerService',
  useExisting: LoggerService,
};

@Module({
  providers: [LoggerService, loggerAliasProvider],
})
export class AppModule {}

위의 예시는 loggerAliasProvider 토큰으로 LoggerServide 프로바이더에 액세스 할 수 있도록 했다.

두 종속성이 싱글톤 스코프로 지정되면, 동일한 인스턴스를 공유한다.

Non-Service based providers

프로바이더는 클래스의 인스턴스를 제공하는 것이 대부분이지만,

해당 용도에 국한되지 않고 모든 값을 제공할 수 있다.

const configFactory = {
  provide: 'CONFIG',
  useFactory: () => {
    return process.env.NODE_ENV === 'development' ? devConfig : prodConfig;
  },
};

@Module({
  providers: [configFactory],
})
export class AppModule {}

위의 예시에서는 현재 환경에 따라 다른 Config 배열을 제공하는 코드이다.

Export custom provider

커스텀 프로바이더는 다른 공급자와 마찬가지로 범위가 선언 모듈에 한정된다.

따라서 다른 모듈에서 사용하려면 export 해야 한다.

const connectionFactory = {
  provide: 'CONNECTION',
  useFactory: (optionsProvider: OptionsProvider) => {
    const options = optionsProvider.get();
    return new DatabaseConnection(options);
  },
  inject: [OptionsProvider],
};

@Module({
  providers: [connectionFactory],
  exports: ['CONNECTION'],
})
//or
@Module({
  providers: [connectionFactory],
  exports: [connectionFactory],
})
export class AppModule {}

위처럼 export 하려면 해당 토큰 또는 공급자 객체를 사용할 수 있다.

Asyncronous provider

프로바이더가 비동기적으로 생성되길 원할 수도 있다.

이는 useFactory를 이용해 비동기적으로 생성하면 된다.

{
  provide: 'ASYNC_CONNECTION',
  useFactory: async () => {
    const connection = await createConnection(options);
    return connection;
  },
}

해당 프로바이더의 주입은 다른 프로바이더와 마찬가지로

위의 예시에서는 토큰을 이용해 @Inject('ASYNC_CONNECTION')로 주입하면 된다.


참고

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Progamming), FP (Functional Programming), and FRP (Functional Reac

docs.nestjs.com

마치며

커스텀 프로바이더를 생성해 본 적이 많이 없어서

위의 기능들이 편리하다는 것을 체감하기 힘들었다.

많은 프로젝트를 해보면서 좀 더 익숙해지면, 오늘보다 더 많은 것들을 볼 수 있을 것 같다.

 

잘못된 정보에 대한 피드백은 환영입니다.

감사합니다.

728x90