Published on

nestjs custom provider 에 대해서

Authors
  • avatar
    Name
    길재훈
    Twitter

Custom Provider

Docs 를 살펴보면서, Dynamic Module 을 같이 보고 있는데, 역시나 모든것이 연관되어 있다. Dynamic Module 을 조금이나마 더 자세히 이해하기 위해서는 Custom Provider 를 더 알 필요성이 느껴졌다. Nest 를 더 많이 이해하고, 더 능숙하게 다루기 위해서는 기본 개념에 대해서 잘 알아야 한다. 그런 의미로 Provider 에 대해서 더 자세히 공부하고자 한다.

Provider 의 작동되는 개념

ProviderDINest IoC Container 에 의해 작동된다 다음은 Docs 의 예이다

Service Logic 생성

// cats.service.ts

import { Injectable } from '@nestjs/common'
import { Cat } from './interfaces/cat.interface'

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = []

  findAll(): Cat[] {
    return this.cats
  }
}

Servcie Logic 을 사용할 Controller 생성 이후 Service 주입

// cats.controller.ts

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 ContainerProvider(Service logic) 주입

// cats.module.ts

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 ContainerModule 을 말하는것을 볼 수 있다. 여기서 중요한 부분은 Service Logic 을 실행할 ClassInjectable 데코레이터를 사용한점이다.

Provider 에서 보아서 알겠지만, Injectable 데코레이터를 사용한 로직은 Nest IoC Contianer( Module ) 에서 관리할( or 주입가능한 ) Class 를 선언하는것이다. 이렇게 Injectable 데코레이터가 선언된 ClassNest IoC Container 에서 Provider 로 넣어주면, 등록된 Controller 에서 해당 ClassInstance
Constructor 의 인자로 주입받아 사용가능하게 된다.

이렇게, ClassProvider 로 등록하기만 하면, 그 Class 를 실행시킨 Instance 를 등록된 Controller 에게 주입해주는 것은 Nest IoC Container 에서 알아서 처리해준다, Class 를 직접 주입하는 복잡한 과정없이 해당 기능에만 집중 가능하게 만든다.

물론 위와 같은 과정에서 Controller 는 주입받을 ParameterDI Token(Provier에 주입된 Service) 을 미리 선언해주어야, 해당 ParameterService Instance 주입이 가능하다


 constructor(private catsService: CatsService) {} // <--  이렇게 주입가능하도록 해야만 한다

그렇다면, Provider 의 주입은 이해를 했고, Controller 는 어떻게 생성되고 작동하는지 그 개념적 이해가 있어야지, 앞으로 Custom Provider 를 이해하는데 편리할 것이다 Nest IoC Container 에서 ControllerInstance 화 할때, 첫번째 하는 일은 종속성을 찾는것이다.

위의 LogiccatsService 를 주입받도록 parameter 로 선언되어 있다. Instance 화 과정에서 CatsService 의 종속성을 찾으면 CatsServiceToken 에 대해 조회를 수행한다

이 조회과정은 일반 Cache 와 비슷하다. Provider ServiceInstance 화하고 캐시하거나, 이미 해당 ProviderInstanceCache 되어 있다면, 그 CacheInstance 를 반환한다

이부분은 Nest 가 알아서 처리해주는 굉장히 편리한 기능이다.

실제 종속성을 관리하는 알고리즘인 dependancy Graph 는 훨씬더 복잡하고 정교하게 이루어져 있다고 Docs 에서 설명하고 있다 즉 위의 동작은 굉장히 단순화 시켜 설명한것이라는 점을 강조한다

여기서 Token 이라는 말이 굉장히 중요하다.

TokenDI 진행시 IoC Container 에 등록된 key 값이라고 생각하면 쉽다 앞에서 이미 언급했지만, ControllerInstance 할때,

Instance 화 과정에서 CatsService 의 종속성을 찾으면 CatsServiceToken 에 대해 조회를 수행한다

라고 말했다. 즉, IoC Container 는 종속성에 대해(provider 를 말한다.. ) Token 을 통해, CacheInstance 를 찾거나, 없으면 Tokenkey 값으로 생성해 InstanceCache 한다는 것이다

이러한 과정을 통해, tokenprovider 에 존재한다는 것을 아는것은 중요하다. 다음의 로직을 살펴보면, 이를 더 쉽게 이해할 수 있다.

기존의 cats.module.ts

// cats.module.ts

import { Module } from '@nestjs/common'
import { CatsController } from './cats/cats.controller'
import { CatsService } from './cats/cats.service'

@Module({
  controllers: [CatsController],
  providers: [CatsService], // <-- token 없이 값만 입력한것으로 보인다.
})
export class AppModule {}

위의 로직은 실제로 다음과 같다

// cats.module.ts

import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
import { CatsService } from './cats/cats.service';

@Module({
  controllers: [CatsController],
  providers: [{
    provide: CatsService, // <-- Token 값
    useClass: CatsService, // <-- Provider 의 value
  }],  // <-- 위의 providers: [CatsService] 와 같다
})
export class AppModule {ㅣㅣ

즉, Providers: [CatsService] 는 위의 로직처럼 일일히 작성하는 불편함을 줄이기 위해, Nest 에서 알아서 token 값을 지정해주는것이라 볼 수 있다. 아무래도 위의 로직이 더 명식적이기는 하지만, 작성해야할 Code 양이 많아져 불편할 수 있기는 하다..

이렇게, Module 에서 providers 선언시, Nest 내부에서 token 이 작성되는것을 알 수 있게 되었다. 이제부터, Custom Provider 를 작성하는 개념을 살펴보도록 하자.

Custom Provider

Docs 에서는 Custom Provider 사용하는 경우의 몇가지 예를 말해주고 있다.

  1. NestClass Instance 화 하는 대신 사용자 정의 Instance 를 생성하고자 할때

Class Instance 를 자동적으로 제공될 ClassCache 하거나, CacheInstance 가 있다면, 그 Instance 를 주입한다고 앞에서 설명했다. 그러므로, 위에서는 기본적으로 생성된 Instance를 사용안하고, 이름은 같지만, 개발자가 직접 생성한 다른 Instance 를 주입하고 싶을때 사용하는것을 말하는것 같다.

  1. 두번째 종속성에서 기존 클래스를 재사용하려고 할때

  2. 테스트를 위해 Mock 버전으로 클래스를 재정의할때

Value Provider

useValue 구문을 상요하면 상수 값을 주입할 수 있다 Docs 에서는 Mock 객체를 반환할 Object Value 값으로 작성하는 예시로 설명한다.

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

const mockCatsService = {
  /* mock implementation
  ...
  */
} //< --  이때, 이 객체는 `CatsService` 의 Type Compatibility 에 맞도록
//      작성되어야만 한다

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

위처럼 지정하면 provider tokenCatsService 가 되지만, 그 값은 mockCatsService 가 된다.

위에서 mockCatsServiceCatsServiceinstanceType 호환되는 객체이므로, 적용가능하다.

Object literal 역시 Object 이므로, Type Compatibility 만 된다면, 그 값으로 사용가능하다. 위의 예에서 주입시, CatsServiceClass 타입을 가졌을때를 가정하므로, 타입역시 호환되어야 한다

하지만, ProviderToken 이므로, 언제든지 Token 값 변경이 가능하다. 이전에는 Class Name 이 사용된 Provider token 을 사용했었다. 이는 Constructor based injection 이 사용된 기본 패턴에 의해 match 된다.

하지만 꼭 Class Name 일 필요는 없지 않은가? String 혹은 Symbol 로 사용가능하다

이는 name 값을 return 하는 Custom Provider 이다.

import { UsersService } from './users.service'

@Module({
  providers: [
    UsersService,
    {
      provide: 'NAME',
      useValue: 'JH',
    },
  ],
})
export class UsersModule {}

이를 통해 Service Logic 을 통해 Injection 하는 방법은 다음과 같다

@Injectable()
export class UserServide {
  @Inject('NAME') private  name: // name = 'JH"
}

기본의 방식과는 다르게, 직접 Token 을 작성했다면, 위처럼 @Inject 데코레이터를 사용하여 처리해야 한다.

또한, main.ts 에서는 App.module.ts 에서 이렇게 Providers 를 통해 주입된 값(useValue)을 app.get('token') 을 통해 접근도 가능하다고 하니 참고하자.

Class Provider

useCalss 를 사용하면 Token 이 해결해야할 Class 를 동적으로 결졍 가능하다

Docs 에서는 다음처럼 예시를 보여준다

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

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

위의 로직을보면, 환경변수에 따라 선택되는 configService 가 달라지는것을 볼 수 있다.

이렇게 configService 에 따라, 주입되는 Service Logic 이 달라진다.

Factory Providers

위는 이미 만들어놓은 Service 를 동적으로 선택하여 만들어지지만, 만약 동적으로 Service logic 을 만들어 Provider 에 넣어야 하는 상황이 있다면 어떻게 해야 할까?

이럴때 사용하는 ProviderFactory Provider 이다. 말 그대로 주어지는 Factory function 을 통해 만들어진 Service 로 주입할 Instance 를 만들어낸다

Factory Provider 의 함수는 선택적 인자를 받는다 이렇게 선택적 인자를 제공하는 속성이 inject 속성으로, 만들어진 Factory Function 에 인자에 값을 전달하는 공급자 배열이다.

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

위의 선택적 인자를 넣어서 처리한다. inject 속성을 상용하여, OptionsProvider 를 주입하여 인자값으로 넘기며, 2번째 인자로 객체를 넘기는것을 볼 수 있다

2번째 인자의 객체는 token 값으로 SomeOptionalProvider 를 사용하며, optional 값을 true 로 설정하여, 선택적으로 받을 수 있도록 한다.

위의 로직에서 @Module 을 보면 알겠지만 SomeOptionalProvider 는 주석처리되어있어서, 현재로써는 undefined 로 주입되지 않았다.

useExistsing

ProvideruseExisting 을 사용하면, 이미 존재하는 Service 에 새로운 별칭을 부과할 수 있다

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

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

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

이렇게 위의 @ModuleLoggerService 에 대해서 2개의 이름으로 주입될 수 있다

Provider 처리하는데도 여러가지 방식으로 처리가 가능하다 useFactory 는 필요시 많이 사용되는 패턴으로 보이므로, 지속 사용하면서 알아보아야 겠다.

다음은 Dynamic Module 에대해서 더 공부해볼 예정이다.