NESTJS를 배워보자(12) - Custom providers

yoon·2023년 8월 15일
0

NESTJS를 배워보자

목록 보기
12/21

3. FUNDAMENTALS

Custom providers

nest의 공식문서를 토대로 작성합니다.

이전 챕터에서는 종속성 주입(DI)의 다양한 측면과 Nest에서 어떻게 사용되는지에 대해 살펴보았습니다. 그 중 한 가지 예로 인스턴스를 클래스에 주입하는 데 사용되는 constructor 기반 DI를 들 수 있습니다. DI는 Nest 코어에 기본적으로 내장되어 있습니다.
지금까지는 한 가지 주요 패턴만 살펴봤습니다. 애플리케이션이 더 복잡해지면 DI 시스템의 모든 기능을 활용해야 할 수도 있으므로 좀 더 자세히 살펴보겠습니다.

DI fundamentals

DI는 종속성 인스턴스화를 자체 코드에서 필수적으로 수행하는 대신 IoC 컨테이너에 위임하는 제어의 역전 기법입니다. provider 챕터의 이 예제에서 어떤 일이 일어나는지 살펴봅시다.

먼저 provider를 정의합니다. @Injectable() 데코레이터는 CatsService 클래스를 provider로 표시합니다.

# 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;
  }
}

그 후 Nest가 컨트롤러 클래스에 provider를 주입하도록 요청합니다:

# 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 컨테이너에 provider를 등록합니다:

# app.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 {}

이 과정에는 세 가지 핵심 단계가 있습니다:

  1. cats.service.ts에서 @Injectable() 데코레이터는 Nest IoC 컨테이너에서 관리할 수 있는 클래스로 CatsService 클래스를 선언

  2. cats.controller.ts에서 CatsController는 constructor 주입을 통해 CatsService 토큰에 대한 종속성을 선언:

constructor(private catsService: CatsService)
  1. app.module.ts에서 토큰 CatsServicecats.service.ts 파일의 CatsService 클래스와 연결.

이 연결이 정확히 어떻게 발생하는지 살펴보겠습니다.

Nest IoC 컨테이너가 CatsController를 인스턴스화할 때 먼저 모든 종속성을 찾습니다. CatsService 종속성을 찾으면 등록 단계(위 3)에 따라 CatsService 토큰에 대한 조회를 수행하여 CatsService 클래스를 반환합니다. 기본 동작인 싱글톤 범위로 가정하면 Nest는 CatsService 인스턴스를 생성하여 캐시한 후 반환하거나 이미 캐시된 인스턴스가 있는 경우 기존 인스턴스를 반환합니다.

위 설명은 요점을 설명하기 위해 단순화함. 종속성에 대한 코드 분석 프로세스가 매우 정교함. 종속성 분석은 종속성이 올바른 순서로 해결되도록 보장. 이 메커니즘은 개발자가 복잡한 종속성 그래프를 관리할 필요를 덜어줌.

Standard providers

@Module() 데코레이터를 봅시다.

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

프로바이더 프로퍼티는 프로바이더 배열을 받습니다. 지금까지는 클래스 이름 목록을 통해 이러한 프로바이더를 제공했습니다.

providers: [CatsService]

이 구문은 아래를 짧게 표현한 것입니다.

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

여기서는 "토큰" CatsService를 "클래스" CatsService와 명확히 연결하고 있습니다. 약식 표기는 토큰이 같은 이름의 클래스 인스턴스를 요청하는 데 사용되는 가장 일반적인 사용 사례를 단순화하기 위한 편의상 표기일 뿐입니다.

Custom providers

표준 프로바이더가 제공하는 요구 사항을 초과하는 요구 사항이 있다면 어떨까요?
예시:

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

Nest를 사용하면 이러한 경우를 처리할 사용자 정의 프로바이더를 정의할 수 있습니다. 사용자 정의 프로바이더를 정의하는 몇 가지 방법을 살펴봅시다.

HINT
종속성 해결에 문제가 있는 경우 NEST_DEBUG 환경 변수를 설정하여 시작 중에 추가 종속성 해결 로그를 가져올 수 있음.

Value providers

useValue

useValue 구문은 상수 값을 주입, 외부 라이브러리를 Nest 컨테이너에 넣기, 실제 구현을 모의 객체로 대체할 때 유용합니다.
Nest가 테스트 목적으로 모의 CatsService를 사용하도록 강제하고 싶다고 가정해 봅시다.

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

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

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

이 예에서 CatsService 토큰은 mockCatsService 모의 객체로 resolve 됩니다. 이 경우 useValueCatsService 클래스와 동일한 인터페이스를 가진 리터럴 객체가 필요합니다.
TS의 구조적 타이핑으로 인해 리터럴 객체 또는 new로 인스턴스화된 클래스 인스턴스를 포함하여 호환되는 인터페이스를 가진 모든 객체를 사용할 수 있습니다.

Non-class-based provider tokens

지금까지는 클래스 이름을 프로바이더 토큰으로 사용했습니다. 이는 constructor 기반 주입에 사용되는 표준 패턴과 일치하며 토큰은 클래스 이름이기도 합니다.
때로는 문자열이나 심볼을 DI 토큰으로 사용하는 유연성을 원할 수도 있습니다.

import { connection } from './connection';

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

이 예에서는 문자열 값 토큰 'CONNECTION'을 외부 파일에서 가져온 기존 연결 객체에 연결하고 있습니다.

NOTICE
토큰 값으로 문자열을 사용하는 것 외에도 JS 심볼, TS 열거형 사용 가능.

앞서 표준 constructor 기반 주입 패턴을 사용하여 프로바이더를 주입하는 방법을 살펴봤습니다. 이 패턴을 사용하려면 의존성을 클래스 이름으로 선언해야 합니다. 'CONNECTION' 사용자 정의 프로바이더는 문자열 값 토큰을 사용합니다. 이러한 프로바이더를 주입하는 방법을 살펴봅시다. 이를 위해 @Inject() 데코레이터를 사용합니다. 이 데코레이터는 토큰이라는 단일 인수를 받습니다.

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

위의 예시에서는 예시용으로 'CONNECTION' 문자열을 직접 사용했지만 깔끔한 코드 정리를 위해서는 constants.ts와 같은 별도의 파일에 토큰을 정의하는 것이 가장 좋습니다. 토큰을 자체 파일에 정의하고 필요한 경우 가져오는 심볼이나 열거형과 마찬가지로 취급하면 됩니다.

Class providers

useClass

useClass 구문을 사용하면 토큰이 확인해야 하는 클래스를 동적으로 결정할 수 있습니다. 예를 들어 추상 또는 기본 ConfigService 클래스가 있다고 가정합니다. 현재 환경에 따라 Nest에서 다른 구성 서비스 구현을 제공하고자 합니다.

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

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

먼저 리터럴 객체로 configServiceProvider를 정의한 후 @Module() 데코레이터의 프로바이더 프로퍼티에 전달한 것을 알 수 있습니다. 이것은 약간의 코드 구성일 뿐이지만 기능적으로는 이 장에서 지금까지 사용한 예제와 동일합니다.

Factory providers

useFactory

useFactory 구문을 사용하면 프로바이더를 동적으로 생성할 수 있습니다. 실제 프로바이더는 factory 함수에서 반환된 값으로 제공됩니다. factory 함수는 필요에 따라 간단하거나 복잡할 수 있습니다. 단순한 factory는 다른 프로바이더에 의존하지 않을 수 있습니다. 더 복잡한 factory는 결과를 계산하는 데 필요한 다른 프로바이더를 자체적으로 주입할 수 있습니다. 후자의 경우 factory 프로바이더 구문에는 한 쌍의 관련 메커니즘이 있습니다:

  1. factory 함수는 (선택적) 인수를 받을 수 있음.
  2. (선택사항) inject 프로퍼티는 인스턴스화 프로세스 중에 Nest가 확인하여 factory 함수에 인수로 전달할 프로바이더 배열을 허용. 또한 이러한 프로바이더는 선택 사항으로 표시할 수 있음.
    두 목록은 서로 연관되어 있어야 함: Nest는 inject 목록의 인스턴스를 동일한 순서로 factory 함수에 인수로 전달. 예:
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 구문을 사용하면 기존 프로바이더에 대한 별칭을 만들 수 있습니다. 이렇게 하면 동일한 프로바이더에 액세스할 수 있는 두 가지 방법이 생성됩니다. 아래 예제에서 문자열 기반 토큰인 'AliasedLoggerService'는 클래스 기반 토큰인 LoggerService의 별칭입니다. 'AliasedLoggerService'에 대한 종속성과 LoggerService에 대한 종속성이 각각 하나씩 있다고 가정합니다. 두 종속성 모두 싱글톤 범위로 지정되면 둘 다 동일한 인스턴스로 resolve 됩니다.

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

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

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

결국 useExisting을 사용하면 별칭으로 프로바이더에 공급할 수 있다는 말입니다.

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 하려면 토큰이나 전체 프로바이더 객체를 사용할 수 있습니다.

토큰을 사용하여 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 {}

고생하셨습니다!
다음 글에서 만나요~~😀


저도 아직 배우는 단계입니다. 지적 감사히 받겠습니다. 함께 열심히 공부해요!!

profile
백엔드 개발자 지망생

1개의 댓글

comment-user-thumbnail
2023년 8월 15일

이런 유용한 정보를 나눠주셔서 감사합니다.

답글 달기