2편에서는 provider wrapper를 잠깐 살펴봤었다. 후에 나올 dynamic module을 이해하기 위해서는 factory provider를 알아야 하는데, 이를 위해 custom provider를 살펴보자.

Custom provider

custom provider란 말 그대로 provider를 wrapper 형식으로 custom하게 등록할 수 있게 해준 것이다. wrapper 형식은 다음과 같다.

{
  provide: <token>,
  use~~: <provider> 
}

use~~ 형식에는 useValue, useClass, useExisting, useFactory 이 4개를 쓸 수 있는데, 문서에 더 자세하게 나와있으므로 여기서는 그냥 대충 보자. [참고]

useValue

useValue는 말 그대로 상수값을 provider로 등록하는 것이다. mocking 등에 쓰일 수 있다.

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

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

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

테스트 목적으로 CatService에 대신 mockCatService를 넣고 싶을 때 이를 사용할 수 있다. 꼭 object 형태가 아니더라도 string, 배열 등 값이면 뭐든 가능하다.

useClass

이건 우리가 아는 일반적인 provider이다. 다만 클래스 이름 그대로 쓰지 않고 wrapper로 감싸서 전달하면 dynamic(useFactory보다는 아니지만)하게 전달할 수 있다고는 한다.

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

useFactory

진짜 dynamic하게 provider를 만들 수 있다. 코드로 보면 이해가 빠르다. (factory라는 용어가 oop 디자인 패턴 중 하나인 것으로 알고 있다.)

import { FactoryProvider, Injectable, Module } from '@nestjs/common';

interface CatServiceOption {
  headLeg: string;
}

@Injectable()
class CatServiceOptionProvider {
  head = 'head';
  leg = 'leg';
  arm = 'arm';
}

@Injectable()
class CatService {
  headLeg = '';
  constructor(private readonly catServiceOption: CatServiceOption) {
    this.headLeg = catServiceOption.headLeg;
  }
}

const catServiceFactoryProvider: FactoryProvider = {
  provide: CatService,
  useFactory: (option: CatServiceOptionProvider) => {
    const headLeg = option.head + option.leg;
    return new CatService({ headLeg });
  },
  inject: [CatServiceOptionProvider],
};

@Module({
  providers: [CatServiceOptionProvider, catServiceFactoryProvider],
})
class CatModule {}

CatService를 구성하는 데에 headLegCatServiceOption이 필요한데, 이게 CatServiceOptionProvider라는 다른 곳에서 등록된 provider만이 이를 구성할 수 있는 정보인 headleg를 가지고 있다고 해보자.

그러면 기존의 방식으로는 CatServiceCatServiceOptionProvider를 다 받아와서 직접 headLeg를 만들어야 할 것이다. 근데 headleg만 필요한데 arm까지 다 데려와서 headLeg를 만드는 게 맞는걸까? 간단한 걸 구성하는 데에도 복잡한 dependency가 생길 것이고, 필요한 것만 주입받지 못하게 된다.

공장(factory)의 의미도 뭔가를 찍어낸다는 의미에서 사용된 것이다. 만약 headLeg를 구성하는 방식이 코드처럼 head + leg가 아니라 head + leg + leg 처럼 바뀌어야 된다고 하면, 물건 전체(provider)를 바꾸는 게 아니라 그저 공장에서 부품(option)을 바꾸면 된다. (option.legleg + leg를 넘겨주면 된다.)

다시 코드로 돌아와서, Nest container는 factory provider를 inject에 들어간 token으로 인스턴스를 찾은 다음에 useFactory에 명시된 함수에 parameter로 넣어줘서 반환받은 인스턴스를 provide에 적힌 token으로 등록하는 식으로 처리한다. FactoryProvider의 타입을 보면 이해가 쉬울 것이다. (OptionalFactoryDependency는 이름 그대로 optional dependency이다.)

// packages/common/interfaces/modules/provider.interface.d.ts
export interface FactoryProvider<T = any> {
    provide: InjectionToken;
    useFactory: (...args: any[]) => T | Promise<T>;
    inject?: Array<InjectionToken | OptionalFactoryDependency>;

    ...

}

example

그럼 어느 정도 개념을 이해했으니 실제로 어떻게 쓰이는 지 보면

import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';

TypeOrmModule.forRootAsync({
  imports: [ConfigModule],
  useFactory: (configService: ConfigService) => ({
    type: 'mysql',
    host: configService.get('HOST'),
    port: configService.get('PORT'),
    username: configService.get('USERNAME'),
    password: configService.get('PASSWORD'),
    database: configService.get('DATABASE'),
    entities: [],
    synchronize: true,
  }),
  inject: [ConfigService],
});

위 코드에서 환경변수로 설정해준 db 설정값들이 TypeOrmModule(db에 접근하는 기능을 제공하는 module)을 만드는 데에 들어가게 된다. (ConfigService는 환경변수에 접근할 수 있는 NestJS에서 제공하는 기본 provider이다.)

아니 근데 이거는 provider가 아니라 module이고 forRootAsync라는 이상한 것도 있고 useFactoryinject말고는 겹치는 게 없잖아 라는 생각이 당연히 들 것이다. factory provider의 기능은 바로 dynamic module에서 빛을 발하는데, 4편에서 살펴보자.

Reference