[NestJS] Provider란

Gon Kim·2022년 11월 21일
0

1. Provider

Documentation | NestJS - A progressive Node.js framework

IoC

  • 서로 뒤엉킨 의존관계를 갖는 객체들의 초기화 작업을 코드 상에서 선언적으로 하는 것이 아니라, 외부, 특히 IoC 컨테이너라는 곳에 위임시키는 것이다. nestjs에서는 nestJs runtime에 이 작업이 이루어지도록 nestjs가 일하고 있다.

Provider

  • nestjs에서 사용하는 단어로, 기본적으로 이 친구는 class이다. class 중 다른 class에 주입될 수 있는 의존성으로 쓰이는 class를 가리킨다.
  • 다른 class에게 inject되는, 제공되는 것이 핵심 아이디어이다.
  • module에 provider임을 명시해줘야한다.

@Injectable()

  • 해당 class가 nestJs IoC 컨테이너에 의해 관리되어야할 대상임을 알려주는 데코레이터

DI

  • catsService class를 선언하고, injectable 데코레이터를 달아뒀다고 하자.
  • 이를 주입하고자 하는 class의 생성자를 통해 해당 service를 주입할 수 있다. constructor(private catsService: CatsService) {}
  • netJs는 CatsService 인스턴스를 새로 생성하고, 반환해주거나, singleton으로 관리되는 경우라면 이미 존재하는지 확인하고, 있다면 그 인스턴스를 반환해준다.
    • 이러한 부분을 nestJs IoC 컨테이너가 알아서 해준다.

Scopes

  • 일반적으로 provider들은 scope을 갖는다. scope은 lifetime을 뜻한다.
  • 기본적으로 우리의 서버 app이 시작되면, 모든 의존성들은 resolve된다. 모든 provider 인스턴스들이 초기화되어야한다는 것이다. 또한 app이 종료되면, 모든 인스턴스들은 삭제된다. 당연한 이야기이다.
  • nestjs에서는 provider의 lifetime을 request-scope으로 관리할 수도 있다. 즉, provider 인스턴스의 생성과 소멸 시점을 정의할 수 있고, 그걸 scope이라고 한다.

Custom providers

  • 직접 provider들을 만들 수 있고, 해당 provider들을 정의하는 방법은 여러가지가 있다.
    • plain values, classes, async/sync factory 등을 사용한다.

module.ts

  • provider(service)와 이 provider를 사용한 consumer(controller)를 모두 정의했으면, 이 친구들 사이에 injection이 일어난다는 것을 nestjs에게 알려줘야한다.
  • module.ts에 이런 관계를 명시한다.

Custom Providers

Documentation | NestJS - A progressive Node.js framework

nestjs에서 IoC가 일어나는 과정

IoC는 어떤 과정을 거쳐 일어날까?

우리는 service에 @Injectable() 데코레이터를 붙여주고, controller의 생성자에서 해당 service를 인자로 적어주고, module.ts에 controller, service들을 명시해줌으로써 실제로는 다음과 같은 작업을 했다.

  1. cats.service.ts에서 @Injectable() 데코레이터를 통해 CatsService class가 Nest IoC 컨테이너에 의해 관리되는 class임을 명시한다.
  2. cats.controller.ts에서 CatsController가 생성자단에서 CatsService token에 대한 의존성을 가짐을 명시한다.
    • constructor(private catsService: CatsService)
  3. app.module.ts에서 CatsService token과 CatsService class를 연결시킨다.

이제 Nest IoC 컨테이너는 이런 과정을 거쳐간다.

  • 먼저 CatsConroller 인스턴스를 만들 때, 의존성이 있나 확인한다. CatsService를 발견할 것이다.
  • CatsService token을 뒤져본다. module에 token과 class를 연결해뒀으므로, CatsService Class가 반환된다.
    • service가 singleton으로 관리되는 scope이라고 하면, 이 때 CatsService 인스턴스를 생성하고, 캐싱해둔다.

위와 같은 과정을 dependency analysis라고 한다. 간략하게 설명했지만, 훨씬 복잡하고 application bootstrapping 동안에 일어난다고 한다. 하지만 큰 틀은, 위 과정은 transitive하고, bottom-up 방식이라는 것이다. CatsService가 의존성을 갖는다면, nest IoC 컨테이너는 이 의존성들을 먼저 생성하러 간다. 또한 이 의존성들이 올바른 순서로 resolve될 수 있도록 관리한다.

standard providers

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

일반적으로 볼 수 있는 형태이다.

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

첫 예시에에서 providers 부분은 사실 이 코드의 짧은 버젼일 뿐이다.

provide 부분에 들어가는 것은 token, useClass는 말 그대로 사용할 class를 의미한다.

사실 각 인스턴스들은 provide 부분에 들어가는 token으로 식별되는 것이고, 그에 매핑되는 class를 위처럼 지정해야한다. 첫 예시의 경우 token과 class name을 동일하게 사용했을 뿐이다.

value providers: useValue

다음과 같은 상황에 유용하다고 말한다.

  • provider의 token과 매핑할 대상이 constant value일 경우
  • 외부 라이브러리를 nest Container에서 관리하도록 하고 싶은 경우
  • mock object를 사용하고 싶은 경우
import { CatsService } from './cats.service';

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

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

실제로 쓰던 CatsService 대신 리터럴 오브젝트로 mockCatsService를 만들어 넣어줬다.

Non-class based provider tokens

import { connection } from './connection';

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

token으로 꼭 class 이름을 쓸 필요 없다. 그냥 아무 string을 넣어도 된다.

CONNECTION이라는 string을 외부 파일에서 가져온 connection 객체에 매핑하는 예제이다.

@Inject()

이렇게 class based가 아닌 token을 쓸 때 @Inject() 데코레이터를 써서 의존성을 명시해야한다.

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

class providers: useClass

이 방법은 token과 매핑되는 class를 동적으로 변하도록 만들어준다.

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

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

providers 부분에 그냥 class이름을 적기도 했던 것을 기억해보자. 그 경우 해당 token과 class가 강제로 매핑된다. 하지만 위 예시와 같이 provide, useClass를 풀어서 쓰면, 특정 조건에 특정 class가 매핑되도록 할 수 있다.

factory providers: useFactory

provider를 찍어내는 함수에서 provider를 받아 쓰는 법이라고 생각할 수 있다.

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

useFactory 키워드를 사용하며, 함수를 넣어준다. 해당 함수는 provider로 사용될 객체를 return해주면 된다.

함수는 다른 provider를 인자를 받을 수 있으며, inject 키워드 뒤에 배열로 명시해준다. 해당 의존성들은 useFactory에 매핑된 함수에 들어가기전에 resolve된다.

각 의존성들은 optional 표기를 통해 optional 여부를 표현할 수 있다. optional이 true라면, 없는 경우 undefined로 resolve된다.

alias providers

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

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

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

하나의 provider를 가리키는 2가지의 token을 만드는 법이다. LoggerService는 ‘AliasedLoggerService’와 LoggerService 클래스 이름 두가지 토큰으로 매핑된 것

어디다 쓸 수 있을지는 모르겠다.

export

provider들은 해당 provider가 선언된 module를 scope으로 갖는다. 해당 module이 nest IoC 컨테이너에 의해 읽힐 때 모두 resolve되는 것이다.

provider를 module 바깥에서도 인지하도록 하기 위해서는 export해야한다.

const connectionFactory = {
  provide: 'CONNECTION',
  useFactory: (optionsProvider: OptionsProvider) => {
    const options = optionsProvider.get();
    return new DatabaseConnection(options);
  },
  inject: [OptionsProvider],
};

@Module({
  providers: [connectionFactory],
  exports: [connectionFactory],
	// exports: ['CONNECTION']
})
export class AppModule {}

token 혹은 provider 자체로 export할 수 있다.

2. 더 알아볼 것

  • module에서는 imports key도 존재한다. 다른 module의 의존성을 import하는 것. 이 친구도 알아봐야할 것이다.
  • provider를 주입하는 다양한 방법을 알아봤다. 테스트 코드를 짤 때 service들을 mocking해야할 필요가 있다. 간단하게 적용해볼 예정이다.
profile
응애

0개의 댓글