11. Custom provider

이전 장에서는 DI(종속성 주입) 의 다양한 측면 과 Nest에서 사용되는 방법을 다루었다 . 이에 대한 한 가지 예는 인스턴스(종종 서비스 제공자)를 클래스에 주입하는 데 사용되는 생성자 기반 종속성 주입이다. 종속성 주입이 기본적인 방식으로 Nest 코어에 내장되어 있다는 사실은 놀라운게 아니다. 지금까지 우리는 하나의 주요 패턴만을 탐색했습다. 애플리케이션이 더욱 복잡해짐에 따라 DI 시스템의 전체 기능을 활용해야 할 수도 있으므로 이에 대해 좀 더 자세히 살펴보자.

 

DI 기본 사항

종속성 주입은 종속성의 인스턴스화를 자신의 코드에서 명령적으로 수행하는 대신 IoC 컨테이너(이 경우 NestJS 런타임 시스템)에 위임하는 제어 역전 (IoC) 기술입니다. 공급자 장에서 설명한 예에서 무슨 일이 일어나고 있는지 살펴보자 .

@Injectable() 데코레이터는 CatsService 클래스를 공급자로 표시한다.

// 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가 공급자를 컨트롤러 클래스에 주입하도록 요청합니다.

// 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 컨테이너에 공급자를 등록합니다.

// 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() 데코레이터는 CatsService 클래스를 Nest IoC 컨테이너에서 관리할 수 있는 클래스로 선언한다.
  2. cats.controller.ts 에서는 CatsController 생성자 주입을 통해 토큰 CatsServic e에 대한 종속성을 선언한다.
  3. app.module.ts 에서는 CatsService 토큰을 cats.service.ts 파일의 CatsService클래스와 연결합니다 . 아래에서 이 연관(등록 이라고도 함)이 어떻게 발생하는지 정확히 살펴보겠다.
 constructor(private catsService: CatsService)

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

*이 설명은 요점을 설명하기 위해 약간 단순화되었다. 우리가 간과한 중요한 영역 중 하나는 코드의 종속성을 분석하는 프로세스가 매우 정교하며 애플리케이션 부트스트래핑 중에 발생한다는 것이다. 주요 기능 중 하나는 종속성 분석(또는 "종속성 그래프 생성")이 전이적 이라는 것이다 . 위의 예에서 CatsService자체에 종속성이 있으면 해당 종속성도 해결된다. 종속성 그래프는 종속성이 올바른 순서(기본적으로 "상향식")로 해결되도록 보장한다. 이 메커니즘을 사용하면 개발자가 복잡한 종속성 그래프를 관리할 필요가 없다.

 

표준 공급자

@Module() 데코레이터를 자세히 살펴보자. app.module 에서는 다음을 선언한다.

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

providers 속성은 providers 의 배열을 사용합니다 . 지금까지 우리는 클래스 이름 목록을 통해 해당 공급자를 제공했습니다. 실제로 이 구문은 providers: [CatsService] 보다 완전한 구문을 축약한 것입니다.

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

 

이제 이 명시적인 구성을 확인했으므로 등록 프로세스를 이해할 수 있을 것이다. 여기서는 CatsService 토큰을 CatsService클래스와 명확하게 연결합니다 . 약칭 표기법은 토큰이 동일한 이름으로 클래스 인스턴스를 요청하는 데 사용되는 가장 일반적인 사용 사례를 단순화하기 위한 편의일 뿐입니다.

맞춤 공급자

요구 사항이 표준 공급자 가 제공하는 사항을 넘어서면 어떻게 할까 ? 다음은 이에대한 몇 가지 예이다.

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

Nest를 사용하면 이러한 사례를 처리하기 위해 맞춤 공급자를 정의할 수 있다. 사용자 지정 공급자를 정의하는 여러 가지 방법을 제공한다. 한번 살펴보자.

 

힌트) 종속성 해결에 문제가 있는 경우 NEST_DEBUG환경 변수를 설정하고 시작 중에 추가 종속성 해결 로그를 얻을 수 있다.

 

공급자 : 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 모의 객체로 확인된다. useValue값이 필요한데, 이 경우 CatsService대체하는 클래스와 동일한 인터페이스를 갖는 리터럴 객체입니다. TypeScript의 구조적 유형 지정 으로 인해 리터럴 객체 또는 new 로 인스턴스화된 클래스 인스턴스를 포함하여 호환 가능한 인터페이스가 있는 모든 객체를 사용할 수 있다.

 

클래스 기반이 아닌 공급자 토큰

지금까지 우리는 공급자 토큰( providers  배열에 나열된 공급자provide 속성  )으로 클래스 이름을 사용했다. 이는 생성자 기반 주입 에 사용되는 표준 패턴과 일치하며 , 여기서 토큰은 클래스 이름이기도 하다. ( 이 개념이 완전히 명확하지 않은 경우 토큰에 대한 내용을 다시 확인하려면 DI 기본 사항을 다시 참조하면 된다.) 때로는 문자열이나 기호를 DI 토큰으로 사용할 수 있는 유연성을 원할 수도 있다.

import { connection } from './connection';

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

이 예에서는 문자열 값 토큰( 'CONNECTION' )을 외부 파일에서 가져온기존 connection 개체와 연결한다.

 

주의) 문자열을 토큰 값으로 사용하는 것 외에도 JavaScript 심벌 또는 TypeScript 열거형을 사용할 수도 있다.

 

표준 생성자 기반 주입 패턴을 사용하여 공급자를 주입하는 방법을 살펴보았다. 이 패턴을 사용하려면 클래스 이름을 사용하여 종속성을 선언해야 한다. 사용자 지정 공급자 'CONNECTION'는 문자열 값 토큰을 사용한다. 그러한 공급자를 주입하는 방법을 살펴보겠다. 이를 위해 @Inject()데코레이터를 사용한다. 이 데코레이터는 토큰이라는 단일 인수를 사용한다.

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

힌트) @Inject() 데코레이터는 @nestjs/common 패키지 에서 가져온다.

 

설명을 위해 위 예제의 'CONNECTION' 문자열을 직접 사용했지만 깔끔한 코드 구성을 위해서는 constants.ts 같은 별도 파일에 정의되어 필요한 곳에서 임포트해 사용한다.

 

클래스 공급자 : useClass

 useClass구문을 사용하면 토큰이 해결되어야 하는 클래스를 동적으로 결정할 수 있다. 예를 들어 추상(또는 기본) ConfigService 클래스가 있다고 가정해 보겠다. 현재 환경에 따라 Nest가 구성 서비스의 다른 구현을 제공하기를 원할 수 있다. 다음 코드는 이러한 전략을 구현하고 있다.

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

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

이 코드 샘플에서 몇 가지 세부 정보를 살펴보자. 먼저 리터럴 객체로 configServiceProvider 를 정의한 다음 이를 모듈 데코레이터의 providers 속성에 전달한다는 것을 알 수 있다. 이는 단지 약간의 코드 구성일 뿐이지만 이 장에서 지금까지 사용한 예제와 기능적으로 동일하다.

ConfigService또한 클래스 이름을 토큰으로 사용했습니다 . ConfigService 에 의존하는 모든 클래스의 경우 Nest 는 다른 곳에서 선언되었을 수 있는 기본 구현(예: @Injectable() 데코레이터로 선언된 ConfigService  ) 을 재정의하는 제공된 클래스( DevelopmentConfigService또는 ProductionConfigServiceConfigService )의 인스턴스를 주입한다.

 

팩토리 공급자 : useFactory

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

  1. 팩토리 함수는 (선택적) 인수를 받아들일 수 있다.
  2. (선택 사항) inject속성은 인스턴스화 프로세스 중에 Nest가 확인하고 팩토리 함수에 인수로 전달하는 공급자 배열을 허용한다. 또한 이러한 공급자는 선택 사항으로 표시될 수 있다. 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 {}

 

앨리어스 공급자 : useExisting

 useExisting구문을 사용하면 기존 공급자에 대한 별칭을 만들 수 있다. 이는 동일한 공급자에 액세스하는 두 가지 방법을 만든다. 아래 예에서 (문자열 기반) 'AliasedLoggerService' 토큰은 (클래스 기반) LoggerService 토큰의 별칭이다. 두 가지 종속성이 있다고 가정하자. 하나는  'AliasedLoggerService' 를 위한 것이고 다른 하나는 LoggerService 를 위한 것이다. 두 종속성이 모두 SINGLETON 범위로 지정되면 둘 다 동일한 인스턴스로 확인된다.

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

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

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

 

비 서비스 기반 공급자

공급자는 종종 서비스를 제공하지만 해당 용도에만 국한되지는 않는다. 공급자 모든 값을 제공할 수 있다. 예를 들어 공급자는 아래와 같이 현재 환경을 기반으로 구성 개체 배열을 제공할 수도 있다.

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

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

 

맞춤 공급자 내보내기

다른 공급자와 마찬가지로 사용자 지정 공급자의 범위는 선언 모듈로 지정된다. 다른 모듈에서 볼 수 있도록 하려면 익스포트 합니다. 사용자 정의 공급자를 내보내려면 해당 토큰이나 전체 공급자 개체를 사용할 수 있다.

다음 예에서는 토큰을 사용한 익스포트를 보여준다.

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

'Backend(Framework) > NestJS 개요(공식문서 번역)' 카테고리의 다른 글

13. Dynamic modules  (1) 2023.12.02
12. Asynchronous providers  (0) 2023.11.19
10. Custom decorators  (0) 2023.11.19
9. Interceptors  (0) 2023.11.19
8. Guards  (0) 2023.11.19
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유