[NestJS docs] Custom providers

nakkim·2022년 10월 16일
0

NestJS docs

목록 보기
9/10
post-custom-banner

지금까지는 constructor 기반의 DI를 이용했다.
어플리케이션이 복잡해지면, DI 시스템의 다른 기능이 필요할 수 있음
긍까 한번 알아보자

DI fundamentals

DI(의존성 주입)은 의존성의 인스턴스화를 IoC 컨테이너(우리의 경우 NestJS 런타임 시스템)에 위임하는 inversion of control(IoC) 기술이다. 요거에 대해 자세히 알아보자.

먼저, 프로바이더를 정의하자. CatsService 클래스에 @Injectable() 데코레이터를 달아서 프로바이더라고 알린다.

import { Injectable } from '@nestjs/common';

@Injectable()
export class CatsService {}

다음으로 컨트롤러에 프로바이더를 주입하라고 Nest에게 요청한다.

import { Controller } from '@nestjs/common';
import { CatsService } from './cats.service';

@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}
}

마지막으로, Nest IoC 컨테이너에 프로바이더를 등록한다.

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

이게 어떻게 가능한 것일까? 3가지 단계로 나눌 수 있다.

  1. cats.service.ts에서 @Injectable() 데코레이터가 CatsService 클래스를 Nest IoC 컨테이너에 의해 관리되는 클래스로 선언한다.
  2. cats.controller.ts에서 CatsController는 생성자 주입을 통해 CatsService 토큰에 대한 종속성을 선언한다.
    constructor(private catsService: CatsService)
  3. app.module.ts에서 CatsService 토큰을 cats.service.ts 파일의 CatsService와 연결(등록)한다.

그렇다면 연결은 어떻게 발생할까?
Nest IoC 컨테이너가 CatsController를 인스턴스화할 때, 모든 의존성을 찾는다. CatsService 의존성을 발견하면 위의 연결 3단계에 따라 CatsService 클래스를 반환하는 CatsService 토큰을 찾는다. 기본 동작인 싱글톤 스코프라고 가정하면, Nest는 CatsService의 인스턴스를 만들고 캐시한 후 반환하거나 이미 캐시된 경우 기존 인스턴스를 반환한다.

Standard providers

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})

이거는 사실 단순화된 것이고, 아래가 표준이다.

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

코드를 보면 CatsService 토큰을 CatsService 클래스와 연결하는 것을 확인할 수 있다.

Custom providers

  • Nest가 클래스를 인스턴스화하는 대신 사용자 정의 인스턴스를 생성하려는 경우
  • 두 번째 종속성에서 기존 클래스를 재사용하려는 경우(?)
  • 테스트를 위해 모의 버전으로 클래스를 재정의하려는 경우

Nest는 이런 경우들을 위해 커스텀 프로바이더 정의를 허용한다.

HINT
의존성 해결에 문제가 있다면 NEST_DEBUG 환경변수를 설정해서 로그를 확인해보자

Value providers: useValue

useValue 구문은 상수 값을 주입하거나 외부 라이브러리를 Nest 컨테이너에 넣거나, 실제 구현을 mock 개체(모의 개체)로 바꾸는 데 유용하다. 테스트 목적으로 모의 CatsService를 사용하도록 해보자.

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

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

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

이 예제에서, CatsService 토큰은 mockCatsService 모의 개체로 리졸브한다. useValueCatsService와 동일한 인터페이스인 리터럴 개체를 요구한다. TypeScriptdml 구조 타이핑으로 인해 리터럴 개체나 클래스 인스턴스를 포함하여 호환되는 인스페이스가 있는 모든 개체를 사용할 수 있다.

Non-class-based provider tokens

지금까지 우리는 클래스 이름을 프로바이더 토큰으로 사용했다. 이것은 생성자 기반 주입에 사용되는 표준 패턴과 일치한다. DI 토큰으로 문자열이나 심볼을 사용할 수도 있다.

import { connection } from './connection';

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

문자열 토큰인 'CONNECTION'connection 개체를 연결했다.

NOTICE
문자열을 사용하는 대신, 자바스크립트의 심볼이나 타입스크립트의 enums 사용 가능

문자열 토큰을 사용하는 프로바이더의 주입은 @Inject() 데코레이터를 사용한다.

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

위 예에서는 문자열을 그대로 사용했지만 constants.ts 같은 파일에 토큰을 정의해서 사용하는 것이 좋다.

Class providers: useClass

useClass 구문은 리졸브할 클래스를 동적으로 결정할 수 있게 한다. 예를 들어, 추상의 ConfigService가 있다고 하자. 현재 환경에 따라 다른 config service를 구현해보자.

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

@Module({
  providers: [configServiceProvider],
})
export class AppModule {}
  • configServiceProvider를 리터럴 객체로 정의한 다음 모듈 데코레이터의 프로바이더 속성에 전달한다.
  • ConfigService 클래스 이름을 토큰으로 사용했다. ConfigService에 의존하는 모든 클래스에 대해 DevelopmentConfigService 또는 ProductionConfigService의 인스턴스를 주입하여 다른 곳에서 선언된 기본 구현(예: @Injectable() 데코레이터로 선언된 ConfigService)을 재정의한다.

Factory providers: useFactory

useFactory 구문을 사용하면 프로바이더를 동적으로 생성할 수 있다. 실제 프로바이더는 팩토리 함수에서 반환된 값으로 공급된다. 팩토리 함수는 단순하거나 복잡할 수 있는데, 복잡한 팩토리는 결과를 계산하는 데 필요한 다른 프로바이더를 주입할 수 있다. 후자의 경우, 팩토리 프로바이드 구문에는 다음과 같은 관련 메커니즘이 있다.

  1. 팩토리 함수는 인수를 허용할 수 있다.

  2. inject 속성(옵셔널)은 Nest가 확인하며 인스턴스화 과정 중에 팩토리 함수에 인수로 전달할 공급자의 배열을 받는다.

    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

useExisting 구문은 기존 프로바이더에 대한 별칭을 만들 수 있다.

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

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

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

이렇게 하면 LoggerServiceAliasedLoggerService로도 액세스할 수 있다.

Non-service based providers

프로바이더는 어떤 값이든 제공할 수 있다. 예를 들어, 프로바이더가 현재 환경에 따라 configuration 객채 배열을 제공하는 코드를 작성해보자.

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

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

Export custom provider

다른 프로바이더와 같이, 커스텀 프로바이더는 선언된 모듈 범위이다. 다른 모듈에서 사용하려면 export되어야 한다. 커스텀 프로바이더를 export하기 위해서 토큰이나 프로바이더 객체 자체를 이용할 수 있다.

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

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

// 프로바이더 객체 이용
const connectionFactory = {
  provide: 'CONNECTION',
  useFactory: (optionsProvider: OptionsProvider) => {
    const options = optionsProvider.get();
    return new DatabaseConnection(options);
  },
  inject: [OptionsProvider],
};

@Module({
  providers: [connectionFactory],
  exports: [connectionFactory],
})
export class AppModule {}
profile
nakkim.hashnode.dev로 이사합니다
post-custom-banner

0개의 댓글