Custom Providers

이연중·2021년 9월 14일
0

NestJS

목록 보기
11/22

DI Fundamentals


종속성 주입은 IoC 컨테이너에 인스턴스에 대한 관리를 위임하는 것이다.

다음은 CatsService이다.

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를 프로바이더로 표시한다.

그 다음 Nest가 프로바이더를 컨트롤러 클래스에 주입하도록 요청한다.

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

Nest IoC 컨테이너가 CatsController를 인스턴스화할 때 먼저 종속성을 찾는다.

CatsService라는 종속성을 찾으면, CatsService 클래스를 반환하는 CatsService 토큰에 대한 조회를 수행한다.

DEFAULT Scope를 가정하면, Nest는 CatsService의 인스턴스를 만들고 캐시한 다음 반환하거나 이미 캐시가 되어있으면, 기존 인스턴스를 반환한다.

Standard Providers


@Module() 데코레이터를 보겠다. app.module에서 다음을 선언한다.

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

providers 속성은 providers의 배열을 받는다.

실제로 providers: [CatsService] 구문은 축약형이다.

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

이것이 명시적인 구성이며, 이를 통해 등록 프로세스를 이해할 수 있다.

CatsService 토큰을 CatsService 클래스와 명확하게 연관시킨다.

Custom Providers


Nest가 클래스를 인스턴스화하는 대신 사용자 지정 인스턴스를 만들고 싶은 경우

두번째 종속성에서 기존 클래스를 재사용하려는 경우

테스트를 위해 모의(mock) 버전으로 클래스를 재정의하려는 경우

위같은 경우들은 Standard Provider로 충족이 안될 수 있다. 이같은 경우 Custom Providers를 이용해 충족하게 하면 된다.

Custom Provider를 만드는 데는 여러가지 방법이 있다.

Value Providers: useValue


useValue 구문은 상수값을 삽입하거나 Nest 컨테이너에 외부 라이브러리를 넣거나 실제 구현을 mock 객체로 대체하는데 유용하다.

Nest가 테스트 목적으로 mock CatsService를 사용하도록 강제하고싶다고 가정해보자.

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

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

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

이 예에서 CatsServicemockCatsService mock 객체에 매칭된다.

이때 매칭되는 값은 CatsService 클래스와 동일한 인터페이스를 가진 리터럴 객체 or new로 인스턴스화 된 클래스 인스턴스여야 한다.

Non-Class-Based Provider Tokens


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

이는 토큰이 클래스 이름이기도 한 생성자 기반 주입과 함께 사용되는 표준 패턴과 일치한다.

클래스 이름을 사용하지 않고, 토큰에 문자열이나 기호를 사용할 수도 있다.(자바스크립트 심볼 또는 타입스크립트 열거형을 사용할 수도 있음)

import { connection } from './connection';

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

이 예에서는 문자열 값 토큰을 외부 파일에서 가져온 connection 객체와 연결한다.

위 같은 CONNECTION 커스텀 프로바이더는 다음과 같이 주입한다.

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

위와 같인 직접 CONNECTION 문자열을 사용하기보다는 constans.ts와 같은 별도의 파일에 토큰을 정의하는 것이 좋다.

Class Providers: useClass


토큰이 확인해야 하는 클래스를 동적으로 결정할 수 있다.

다음 예는 현재 환경에 따라 Nest가 구성 서비스의 다른 구현을 제공하는 것이다.

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

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

먼저, 리터럴 객체로 configServiceProvider를 정의하고 모듈 데코레이터의 providers 속성에 전달한다.

또한, 토큰으로 ConfigService 클래스 이름을 사용했다. ConfigService에 의존하는 모든 클래스의 경우 Nest는 제공된 클래스(DevelopmentConfigService 또는 ProductionConfigService)의 인스턴스를 삽입한다.

Factory Provider: useFactory


이를 사용하면 동적으로 프로바이더를 만들 수 있다.

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

단순한 팩토리는 다른 프로바이더에 의존하지 않을 수 있고, 복잡한 팩토리는 결과를 계산하는데 필요한 다른 프로바이더를 자체적으로 주입할 수 있다.

후자의 경우 팩토리 프로바이더 구문에는 다음의 메커니즘이 있다.

  1. 팩토리 함수는 인수를 받을 수 있다.(option)
  2. inject 속성은 Nest가 인스턴스화 프로세스중에 확인하고 팩토리 함수에 인수로 전달한 프로바이더 배열을 허용한다. 두 목록은 상호 연관되어 있어야 한다. Nest는 inject 목록의 인스턴스를 동일한 순서로 팩토리 함수에 대한 인수로 전달한다.(option)
const connectionFactory = {
  provide: 'CONNECTION',
  useFactory: (optionsProvider: OptionsProvider) => {
    const options = optionsProvider.get();
    return new DatabaseConnection(options);
  },
  inject: [OptionsProvider],
};

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

Alias Providers: useExisting


이 구문을 사용하면 기존 프로바이더의 별칭을 만들 수 있다.

이는 동일한 프로바이더에 엑세스를 두가지 방식으로 하게 한다.

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

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

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

Non-Service Based Providers


프로바이더가 서비스를 제공하는 경우가 많지만, 해당 용도에 국한되지는 않는다.

프로바이더는 모든 값을 제공할 수 있다.

예를 들어, 프로바이더는 아래와 같이 현재 환경을 기반으로 구성 객체의 배열을 제공할 수 있다.

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

또는 전체 프로바이더 객체를 사용해서 export 한다.

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

참고

https://docs.nestjs.kr/fundamentals/custom-providers

profile
Always's Archives

0개의 댓글