Asynchronous Providers

때떄로, 한 두개의 asynchronous task가 완료된 이후 application을 실행할 필요가 있다. 예를 들어 DB와의 연결이 완료될 확릴될 때까지 요청을 수락하지 않도록 하고 싶을 수 있다. asynchronous provider를 통해 이를 달성할 수 있다.


이를 위한 문법은 useFacotry에서의 async/await이다. factory는 Promise를 return한다. 그리고 await asynchronous task를 수행한다. Nest는 그러한 provider에 의존하는(주입받는) 클래스를 instance화하기 전에 promise가 해결될 때 까지 기다릴 것 이다.


{
  provide: 'ASYNC_CONNECTION',
  useFactory: async () => {
    const connection = await createConnection(options);
    return connection;
  },
}

<팁>
custom provider syntax에 대해 자세히



Injection

Asynchronous provider는 다른 provider 처럼 token에 의해 다른 component에 주입된다. 예를 들어, 위의 코드는 @Inject('ASYNC_CONNECTION')을 사용한다.



Example

The TypeORM Recipe에 asynchronous provider의 상세한 예가 있다.

'NestJS > Docs_FUNDAMENTALS' 카테고리의 다른 글

Custom Providers  (0) 2024.06.17

Custom Providers

앞서, 우리는 Dependency Injection(DI) 의 다양한 측면을 살펴보았다. 그리고 어떻게 Nest에서 이용되는지를 살펴보았다.
예를 들어 instance들을 class에 주입하기 위해서 constructor based DI가 있다. Dependency Injection은 Nest core에 근본적으로 내장되어 있다는 사실은 놀라운 것은 아니다. 지금까지, 한 가지 패턴만을 살펴보았다. 애플리케이션이 좀더 복잡해질 수록 DI의 모든 기능들을 이해해야 한다.



DI fundamentals

Dependency Injection은 Inversion of Control 기술이다. 이는 의존성의 인스턴스화를 자신의 코드에서 명령형으로 수행하는 대신 IoC(NestJS 의 경우 런타임 시스템)에 의임하는 것이다. Providers Chapter에서 예들을 확인할 수 있다.


의존성 주입(Dependency Injection, DI)은 객체가 필요한 의존성을 직접 생성하지 않고 외부에서 주입받는 디자인 패턴이다. 
    이를 통해 코드의 결합도를 낮추고, 더 유연하고 테스트하기 쉬운 코드를 작성할 수 있다.

     NestJS는 DI 컨테이너를 사용하여 의존성을 관리한다. 이 컨테이너는 애플리케이션은 시작될 때 모든 프로바이더를 생성하고, 이를 필요한 곳에 주입함.

첫 번째로, provider를 정의한다. @Injectable() decorator는 CatsService class가 provider임을 나타낸다.

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를 controller class에 주입하도록 요청한다.


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();
  }
}

마지막으로 provider를 Nest IoC container에 등록한다.


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() decorator은 CatsService 클래스를 Nest IoC 컨테이너에 의해 관리될 수 있는 클래스로 선언한다.

  1. cats.controller.ts에서, CatsController생성자 주입을 통해 CatsService 토큰에 대한 의존성을 선언한다.


  constructor(private catsService: CatsService)

  1. app.module.ts에서 CatsService token을 cats.service.ts파일의 CatsService 클래스와 연결한다.


Nest IoC container는 CatsController를 인스턴스화 할 때, 먼저 [모든 dependency]를 찾는다. 만일 CatsService라는 dependency를 발견했다면, 먼저 CatsService token에 대한 조회를 수행하며, 이는 위에서 설명한 등록 단계(#3)대로 CatsService 클래스를 반환한다. SIngleTon 범위(기본 동작)을 가정하면 Nest는 CatsService instance를 생성하고 이를 캐시한 후 반환하거나, 이미 캐시된 instance가 있는 경우 기존 instance를 반환한다.


위의 '모든 dependeny를 찾는다,'는 요점을 설명하기 위해 약간 단순화되었다. 우리가 대충 넘긴 중요한 부분 중 하나는 dependency을 분석하는 과정이 매우 정교하며, 애플리케이션의 bootstrapping 중에 발생한다는 것 이다. 주요 기능 중 하나는 의존성 분석이 전이적(transitive) 이라는 점이다. 위의 예에서 CatsService 자체가 의존성을 가지고 있다면, 그것들도 해결된다. 의존성 그래프는 의존성이 올바른 순서로 해결되도록 보장한다. - 기본적으로 "bottom up". 이 메키니즘은 개발자가 이러한 복잡한 의존성 그래프를 관리할 필요가 업도록 한다.


 BootStrapping: 소프트웨어 시스템을 초기화하고 실행하기 위해 필요한 초기 단계를 설정하는 과정을 의미한다.

  NestJS에서는 애플리케이션을 시작하고 모든 필수 모듈과 서비스를 초기화하는 과정을 의미한다. NestJS는 bootstrapping 과정에서     IoC 컨테이너를 설정하고, 모든 의존성을 해결하며, HTTP 서버를, 애플리케이션을 실행할 준비를 한다.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

NestJS 부투스트래핑 과정

1. NestFactory.create(AppModule):
    - 애플리케이션의 루트 모듈인 AppModule을 기반으로 NestJS 애플리케이션 instance를 생성
    - 이 과정에서 NestJS는 IoC 컨테이너를 설정하고, 모든 module과 provider를 스캔하여 의존성 그래프를 생성한다.

2. module 및 provider 초기화:
    - NestJS는 AppModule과 그 하위 모듈들을 초기화한다.
    - 각 module의 provider와 controller를 instance화하고, 필요한 의존성을 주입한다.

3. 애플리케이션 설정:
    - NestJS 애플리케이션 instance가 생성되면, 필요한 설정(예, 미들웨어, 파이프, 필터 등)을 적용한다.

4. 서버 시작:
    - app.listen(3000)을 호출하여 HTTP 서버를 시작한다.
    - 애플리케이션이 지정된 포트에서 클라이언트 요청을 수신할 준비를 마친다.


NestJS는 IoC(제어의 역전) 컨테이너는 의존성 주입을 통해 객체의 생성과 관리를 담당한다. 다음은 CatsController가 instance가 생성될 때 벌어지는 일련의 과정이다.

1. 의존성 탐색:
    - Nest IoC 컨테이너는 CatsController를 인스턴스화할 때, CatsController가 의존하는 모든 서비스를 찾는다.
    - 이 과정에서 생성자 주입을 통해 선언된 CatsService 의존성을 확인한다.

2. 토큰 조회:
    - CatsService 의존성을 찾으면, IoC 컨테이너는 CatsService 토큰에 대한 조회를 수행한다.
    - 이 조회는 등록 단계에서 설정된 바와 같이 CatsService 클래스를 반환한다.

3. 싱글톤 범위 관리:
    - 기본적으로 NestJS는 모든 provider를 SingleTon 범위로 관리한다.
    - 이는 애플리케이션 전체에서 해당 provider의 단일 instace를 생성하고 이를 재사용함을 의미한다.

4. 인스턴스 생성 및 캐싱:
    - IoC 컨테이너는 CatsService 클래스의 인스턴스가 이미 생성되어 있는지 확인한다.
    - 만약 instace가 없다면, 새로운 instance를 생성하고 이를 cache에 저장한다.
    - 이미 instance가 존재한다면, 기존의 cache된 instance를 반환한다.

5. 의존성 주입:
    - CatsService 인스턴스가 준비된다면, IoC 컨테이너는 이를 CatsController의 생성자에 주입한다.
    - 이렇게 주입된 CatsService instance는 CatsController내에서 사용된다. 


예제 코드와 과정


// example.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class ExampleService {
  getExample(): string {
    return 'This is an example service';
  }
}


// cats.service.ts
import { Injectable } from '@nestjs/common';
import { ExampleService } from './example.service';

@Injectable()
export class CatsService {
  constructor(private readonly exampleService: ExampleService) {}

  getCats(): string {
    return `This action returns all cats and ${this.exampleService.getExample()}`;
  }
}


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

@Module({
  controllers: [CatsController],
  providers: [CatsService, ExampleService],
})
export class CatsModule {}


// app.module.ts
import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class AppModule {}

동작 과정 요약


NestJS의 DI System은 DI graph를 생성하고 이를 통해 모든 Dependency을 올바른 순서로 해결한다. 이 과정은 전이적(Transitive) 으로 작동하므로, 특정 서비스가 다른 서비스에 의존하는 경우에도 모든 의존성이 해결된다. 이러한 메커니즘은 개발자가 복잡한 의존성 그래프를 관리할 필요 없이, NestJS가 자동으로 이를 처리해주어 개발의 편의성과 유지보수성을 높여준다.


  1. NestJS 애플리케이션 초기화

    • 애플리케이션이 시작되면, AppModule이 초기화된다.
    • AppModule은 CatsModule을 import하여 사용
  2. DI 컨테이너 구성

    • NestJS IoC 컨테이너는 CatsModule을 스캔하여 CatsController, CatsService, ExampleService를 등록한다.
  3. 의존성 그래프 생성

    • NestJS는 각 프로바이더의 의존성을 파악하여 의존성 그래프를 생성한다.
    • CatsService가 ExampleService에 의존하므로, ExampleService가 먼저 instance화 된다.
  4. 의존성 해결

    • IoC 컨테이너는 의존성 그래프에 따라 ExampleService를 먼저 instance화하고 캐시한다.
    • 그런 다음 CatsService를 instance화하고 ExampleService의 instance를 주입한다.
    • 마지막으로 CatsController를 instance화하고 CatsService의 instance를 주입한다.
  5. 요청 처리

    • 클라이언트가 '/cats' 엔드포인트로 요청을 보내면, 'CatsController'의 'findAll' method가 호출된다.
    • findAll method는 CatsService의 getCats method를 호출하여 응답으로 반환한다.
    • getCats method는 ExampleService의 getExample method를 호출하여 응답에 포함시킨다.


Standard providers


app.module의 @Module() decorator을 자세히 살펴보자

// 기본 제공 방식
//         설명: 이 방식은 단순히 CatsService 클래스를 providers 배열에 포함시켜 등록하는 것이다.
//         기본 동작: NestJS는 CatsService를 토큰으로 사용하여 동일한 이름의 클래스를 Provider로 등록한다. 
//            이는 가장 일반적이고 간단한 사용 사례이다.
//         사용 사례: 토큰과 클래스 이름이 동일할 때 가장 많이 사용된다.

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

providers 속성은 providers의 array를 받을 것 이다. 지금까지 우리는 이러한 provider들을 class name을 통해 공급받았다.
사실은 providers: [CatsService] 따위의 구조는 아래의 복잡한 코드의 약식 표기법이다..


// 명시적 제공 방식
//        설명: 이 방식은 Provider 객체를 명시적으로 정의하여 provide 속성과 useClass 속성을 사용해 등록한다.
//        동작 방식: provide 속성은 토큰으로 지정하고, useClass 속성은 해당 토큰에 대해 사용할 클래스를 지정한다.
//        사용 사례: 더 복잡한 시나리오에서 유용하다. 예를 들어, 동일한 토큰에 대해 다른 클래스를 사용하거나, 
//            팩토리 함수나 값을 사용할 때 사용된다.

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


활용 예제

//==============================================================
// mock-cats.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class MockCatsService {
  fetchCats(): string {
    return 'This action returns mock cats';
  }
}

//==============================================================
// cats.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class CatsService {
  getCats(): string {
    return 'This action returns all cats';
  }
}

//==============================================================
// cats.controller.ts
import { Controller, Get } from '@nestjs/common';
import { CatsService } from './cats.service';

@Controller('cats')
export class CatsController {
  constructor(private readonly catsService: CatsService) {}

  @Get()
  findAll(): string {
    return this.catsService.getCats();
  }
}


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

@Module({
  controllers: [CatsController],
  providers: [
    {
      // provide 키는 provider의 token을 지정한다.
      // 이 토큰은 의존성을 주입할 때 사용된다.
      // 일반적으로 클래스 이름을 토큰으로 사용하지만, 문자열이나 심볼을 사용할 수 있다.
      provide: CatsService, // CatsService라는 이름으로 주입

      // useClass 키는 주입할 클래스(또는 provider)를 지정한다.
      // 이 클래스는 지정된 토큰과 연결된다.
      // useClass에 지정된 클래스의 인스턴스가 생성되어 해당 토큰에 할당된다.
      useClass: MockCatsService, // 실제 클래스는 MockCatsService

      // 간단하게 이름은 CatsService이고 본체는 MockCatsService이다.
      // provide를 CatsService, useClass를 MockCatsService로 설정하면
      // CatsService 토큰을 통해 MockCatsService의 인스턴스를 주입받게 된다.
      // 이는 NestJS IoC 컨테이너가 CatsService 토큰을 요청할 때 실제로는 MockCatsService 클래스를
      // 사용하여 instace를 생성하고 반환하기 때문이다. 
    },
  ],
})
export class CatsModule {}

//==============================================================
// app.module.ts
import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class AppModule {}

//==============================================================
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

지금은 이러한 명확한 구조를 통해, registration process를 이해할 수 있다. 여기서 CatsService 토큰을 CatsService 클래스와 명확하게 연결하고 있다. 약식 표기법은 단지 가장 일반적인 사용사례를 단순화하기 위한 편의일 뿐이며, 이 경우 토큰은 동일한 이름의 클래스 인스턴스를 요청하는 데 사용된다.



Custom Providers

만일 요구 사항이 provider가 제공하는 범위를 넘어설 때는 어떻게 해야하는가?, 다음은 몇 가지 예이다.


  • Nest가 class를 instance화(또는 캐시된 인스턴스를 반환)하는 대신, 사용자 정의 instance를 생성하고 싶다.
  • 기존의 class를 두 번째 depedency에서 재사용하고 싶다.
  • test를 위해 class를 모의(mock)버전으로 재정의하고 싶다.

Nest는 이러한 경우를 handler할 수 있도록 Custom provider를 정의하게 해준다. 이는 몇 가지 방법이 존재한다.


<팁!>
만일 dependency에 관련해 문제가 있다면, NEST_DEBUG 환경 변수를 설정할 수 있다. 그리고 startup하는 동안 추가적인 dependency resolution logs를 얻을 수 있다.



Value providers: useValue

useValue 문법은 constant value를 주입하거나, 외부 라이브러리를 Nest 컨테이너에 넣거나, 실제 구현을 모의(mock) 객체로 대체하는 데 유용하다. 예를 들어 Nest가 모의(mock) CatService를 사용하도록 강제하고 싶다고 가정해보자.


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

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

@Module({
  imports: [CatsModule],
  providers: [
    {
      provide: CatsService, // CatsService를 주입하다고 하면
      useValue: mockCatsService, // 실제로 주입되는 것은 mockCatsService 이다.
    },
  ],
})
export class AppModule {}

위의 예에서, CatsService token은 mockCatsService의 mock object로 해결된다. useValue는 값을 필요로 하는데, 이 경우 CatsService 클래스를 대체하는 동일한 인터페이스를 가진 리터럴 객체이다. TypeScript의 구조적 타이핑 덕분에, 리터럴 객체나 new 키워드로 instance화된 class instance를 포함하여 호환 가능한 interface를 가진 어떤 객체도 사용할 수 있다.


// 구조적 타이핑(Structural Typing): 객체의 형태(구조)에 따라 타입을 결정하는 방식이다. 이는 객체가 특정 인터페이스나 타입을 만족하려면, 그 객체가 필요한 속성들을 모두 가지고 있어야 한다는 의미이다. 객체의 실제 타입보다는 객체의 구조와 속성이 중요하다.

// 아래의 John 객체는 Person 인터페이스에 만족한다. name과 age 속성을 모두 가지고 있고, 타입도 일치하기 때문이다.
interface Person {
  name: string;
  age: number;
}

const john: Person = {
  name: 'John Doe',
  age: 30
};


//literal object: 중괄호({})를 사용하여 직접 생성된 객체를 말한다. TypeScript와 JavaScript에서는 객체를 간단하게 생성하기 위해 객체 리터럴을 사용한다. 객체 리터럴은 속성과 값을 한 번에 정의할 수 있어, 코드 작성이 간편하고 가독성이 좋다.

// 객체 리터럴을 사용하여 객체를 생성
const person = {
  name: 'John Doe',
  age: 30,
  greet: function() {
    console.log('Hello, my name is ' + this.name);
  }
};

// 객체의 속성에 접근
console.log(person.name); // 'John Doe'
console.log(person.age); // 30

// 객체의 메서드를 호출
person.greet(); // 'Hello, my name is John Doe'


Non-class-based provider tokens

지금까지, class name을 provider token으로 사용해왔다.(providers array 안에서의 provide 속성의 값) 이러한 standard한 pattern은 constructor based injection에서 사용됬다. (토큰의 개념이 이해가 안된다면 DI Fundatmentals 참고 ) 때때로, DI 토큰으로 string이나 symbol을 사용하는 유연성이 필요할 수 있다.


===토큰의 개념 설명===
token은 NestJS에서 의존성 주입(Dependency Injection,DI) 시스템이 특정 provider를 식별하고 주입하기 위해 사용되는 Key 이다.
token은 provider를 고유하게 식별하는 역할을 하며, provider를 요청할 때 사용된다. 일반적으로 class의 이름을 token으로 사용하지만, string이나 symbol도 token으로 사용할 수 있다.

===왜 토큰을 사용하는가?===
토큰은 의존성을 관리 주입하는데 있어 중요한 역할을 한다. 이를 통해 DI container가 어떤 객체를 생성하고 주입해야 하는지 명확히 알 수 있다. 또한, 동일한 interface를 구현하는 여러 클래스가 있을 때, 어떤 구현체를 주입할지 지정하는 데 유용하다.

===토큰의 유형===
1. 클래스 기반 토큰
    - 클래스 자체를 토큰으로 사용하는 가장 일반적인 방법
    - 클래스 이름을 토큰으로 사용하여 의존성을 주입

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

    @Injectable()
    export class CatsService {
      getCats(): string {
        return 'This action returns all cats';
      }
    }

    @Module({
      // 클래스 자체가 토큰으로 사용
      // 다른 클래스에서 CatsService를 의존성으로 주입받을 수 있다.
      providers: [CatsService],
    })
    export class CatsModule {}

    // Controller    
    import { Injectable } from '@nestjs/common';
    import { CatsService } from './cats.service';

    @Injectable()
    export class CatsController {
      constructor(private readonly catsService: CatsService) {}

      getCats(): string {
        return this.catsService.getCats();
      }
    }

2. 문자열 토큰
    - 문자열을 토큰으로 사용하여 의존성을 주입할 수 있다.
    - 주로 특정 상수나 외부 자원을 주입할 때 사용

    // Module
    import { Module } from '@nestjs/common';
    import { connection } from './connection';  // 외부에서 connection 객체를 가져옴

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

    // Service 
    import { Injectable, Inject } from '@nestjs/common';

    @Injectable()
    export class SomeService {
      constructor(@Inject('CONNECTION') private readonly connection: any) {}

      useConnection() {
        console.log(this.connection);
      }
    }

3. 심볼 토큰
    - 심볼을 사용하여 고유한 토큰을 생성할 수 있다.
    - 주로 고유성이 필요한 경우나 복잡한 의존성 관리가 필요할 때 사용된다.

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

    const CATS_SERVICE_TOKEN = Symbol('CATS_SERVICE_TOKEN');

    @Module({
      providers: [
        {
          provide: CATS_SERVICE_TOKEN,
          useClass: CatsService,
        },
      ],
      exports: [CATS_SERVICE_TOKEN],
    })
    export class CatsModule {}

    // Controller    
    import { Injectable, Inject } from '@nestjs/common';

    @Injectable()
    export class CatsController {
      constructor(@Inject(CATS_SERVICE_TOKEN) private readonly catsService: CatsService) {}

      getCats(): string {
        return this.catsService.getCats();
      }
    }

===요약===
- 토큰의 역할: 의존성 주입 시스템에서 provider를 식별하고 주입하는 데 사용된다.
- 토큰의 유형: 클래스, 문자열, 심볼 등이 있으며, 각 유형은 고유한 상황에 맞게 사용된다.
- 유연성 제공: 토큰을 사용함으로써 다양한 형태의 의존성을 주입할 수 있으며, 특히 테스트나 외부 자연 주입 시 유용

아래의 코드에서 string-valued token('CONNECTION')을 사용했다. 그리고 이미 만들어 둔 external file인 connection object를 사용했다.

import { connection } from './connection';

@Module({
  providers: [
    {
      // 비 클래스 기반 provider token을 사용하는 예제이다. 
      // 이 예제에서는 문자열 CONNECTION을 토큰으로 사용하여 특정 값을 주입한다.
      provide: 'CONNECTION',
      useValue: connection,
    },
  ],
})
export class AppModule {}

<주의사항>
token value로써 string 뿐만 아니라, JS의 symbols랑 TS의 enums를 사용할 수 있다.


이전에 standard constructor based injection pattern을 사용하여 provider를 inject하는 방법을 보았다.
이 pattern은 dependency가 class name으로 선언되어야 한다. 'CONNECTION' custom provider는 string value을 가진 토큰을 사용한다. 이러한 provider를 주입하는 방법을 살펴보자. 이를 위해 @Inject() decorator를 사용해야 한다. 이 decorator은 하나의 인자 즉 token을 받는다.


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

<팁!>
@Inject() decorator는 @nestjs/common 패키지에서 import한다.


위의 코드에서는 바로 'CONNECTION' string을 사용했지만, clean code를 위해서는 다른 file(constants.ts)에 정의된 것을 import하는 것이 좋다. 필요할 때마다 자신만의 파일에 정의하고 필요한 곳에서 import하는 symbol이나 enums 처럼 다뤄야한다.



Class Providers: useClass

useClass 구문을 사용하면 토큰이 해결해야 하는 클래스를 동적으로 결정할 수 있다. 예를 들어, ConfigService class를 가지고 있다고 가정하자. 현재의 environment에 따라, Nest가 다른 configuration service를 실행할 수 있도록 할 수 있다.

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

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

위의 코드에서 몇 개의 디테일을 살펴 보자. 첫 번째로 literal object로 configServiceProvider를 정의했다. 이는 module decorator의 providers 속성에 전달된다. 이는 약간의 다른 형태지만, 기능적으로는 앞에서 살펴본 것들과 동일하다.


두 번째로 ConfigService class name을 토큰으로 사용했다. ConfigService에 의존하는 모든 class에 대해, Nest는 제공된 class('DevelopmentConfigService' 또는 'ProductionConfigService')의 instance를 inject하여, 다른 곳에서 선언된 기본 구현(예, @Injectable() decorator로 선언된 ConfigService)을 재정의 한다.



Factory providers: useFactory

useFactory 구문은 provider을 동적으로 만들게 해준다. 실제 provider은 factory function에 의해 반환된 value에 의해 제공될 것 이다. factory function은 필요에 따라 simple하거나 complex 할 것 이다. simple factory는 다른 provider에 의존하지 않을 수 있다. complex한 factory는 결과를 계산하는 데 필요한 다른 provider를 자체적으로 주입할 수 있다. 후자의 경우, factory provider 구문에는 관련된 두 가지 메커니즘이 있다.


  1. factory function은 (optional) argument를 받을 수 있다.

  2. (optional) inject property는 Nest가 해결하고 instance화 과정에서 factory 함수에 argument로 전달할 provider array을 받는다. 또한, 이 provider는 선택적으로 표시될 수 있다. 두 목록은 상호연관되어야 한다. Nest는 'inject' 목록의 instance를 factory function의 argument로 같은 순서로 전달한다.


const connectionProvider = {
  provide: 'CONNECTION',  
  // useFactory: 동적으로 프로바이더를 생성하는 팩토리 함수를 정의한다.
  // optionsProvider: 필수 의존성
  // optionalProvider: 선택적 의존성으로, 문자열이 될 수 있으며, 주입되지 않으면 undefined가 될 수 있다.

  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 {}
// ===================================================
// 1. 의존성 서비스 정의
import { Injectable, Optional } from '@nestjs/common';

@Injectable()
export class DependencyService {
  getDependencyValue() {
    return 'Dependency Value';
  }
}

@Injectable()
export class OptionalService {
  getOptionalValue() {
    return 'Optional Value';
  }
}

// ===================================================
// 2. 복잡한 factory provider 정의
const complexFactory = {
  // provide: provider의 token인 COMPLEX_FACTORY를 정의
  provide: 'COMPLEX_FACTORY',
  // dependencyService를 사용하여 dependencyValue를 가져오고, optionalService가 주어지면, optionalValue를 가져오고, 그렇지 않으면 No Optional Service를 사용한다. 최종적으로 두 값을 조합하여 문자열을 반환한다.
  useFactory: (
    dependencyService: DependencyService,
    optionalService?: OptionalService
  ) => {
    const dependencyValue = dependencyService.getDependencyValue();
    const optionalValue = optionalService ? optionalService.getOptionalValue() : 'No Optional Service';
    return `Complex Factory Value: ${dependencyValue}, ${optionalValue}`;
  },
  // 팩토리 함수에 주입할 provider 목록을 정의한다. 이 목록의 순서와 factory 함수의 인수 순서는 일치해야한다.
  inject: [DependencyService, OptionalService],  // 주입할 프로바이더 목록
};

// ===================================================
// 2-1.  provide, useFactory, inject에 대한 설명
//     NestJS에서는 의존성 주입(DI)을 통해 애플리케이션의 모듈 간에 객체를 주입하고 관리할 수 있다.
//    provide, useFactory, inject는 provider를 설정할 때 사용하는 중요한 속성이다.
//    이 속성들을 사용하여 동적으로 provider를 생성하고 필요한 의존성을 주입할 수 있다.

// provide
//    역할: provide 속성은 provider의 token을 지정한다. 이 token은 provider를 식별하는데 사용됨
//    유형: 일반적으로 class 이름, string, symbol 등을 사용할 수 있다.
//     사용 예시
    {
      provide: 'SOME_TOKEN',
      useValue: someValue,
    }

// useFactory
//    역할: useFactory 속성은 provider의 값을 동적으로 생성하는 factory function을 지정한다. factory function은 필요한 Dependency를 주입받아 복잡한 로직을 수행하고 값을 반환할 수 있다.
//    반환 값: 팩토리 함수는 provider로 사용할 값을 반환해야 한다.

// inject
//    역할: inject 속성은 factory function에 주입할 provider의 목록을 지정한다. 이 목록의 순서와 factory function의 인수 순서는 일치해야 한다
//    유형: class, token, symbol 등을 사용할 수 있다.
//     사용 예시
    {
      provide: 'SOME_TOKEN',
      useFactory: (dependency1, dependency2) => {
        // 로직 수행
        return someValue;
      },
      inject: [Dependency1, Dependency2],
    }

// ===================================================
// 3. module에서 provider 등록
import { Module } from '@nestjs/common';
import { DependencyService } from './dependency.service';
import { OptionalService } from './optional.service';

@Module({
  providers: [DependencyService, OptionalService, complexFactory],
})
export class AppModule {}

// ===================================================
// 4. service에서 factory provider 사용
import { Injectable, Inject } from '@nestjs/common';

@Injectable()
export class SomeService {
  // COMPLEX_FACTORY 토큰을 사용하여 factory provider에서 생성된 값을 주입받는 서비스를 정의
  constructor(@Inject('COMPLEX_FACTORY') private readonly value: string) {}

  getValue() {
    return this.value;
  }
}


Alias Providers : useExisting


useExising 문법은 기존의 provider에 별칭(aliases) 를 붙여준다. 이는 같은 provider에 접근할 수 있게 하는 두 방법을 제공한다. 예를 들어 string 기반의 AliasedLoggerService는 class 기반의 LoggerService이다. 두 개의 다른 dependency가 있다고 가정하자.(AliasedLoggerService, LoggerService) 만일 두 개의 dependency가 SINGLETON scope로 명시되어 있다면, 둘다 같은 instance를 제공한다.


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

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

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


Non-Service Based Providers

providers는 종종 service를 제공하지만, 그러한 사용으로 제한되지는 않는다. 예를들어, provider는 현재 environment에 기반하여 configuration object를 제공할 것 이다.

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

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


Export custom provider

다른 provider 처럼, custom provider는 선언된 module의 scope로 되어 있다. 만일 다른 module에서 이를 사용하게끔 하고싶다면, 반드시 export 해야한다. custom provider를 export하기 위해서는, provider의 token혹은 전체를 사용해야 한다. 다음 예는 token을 사용한 것 이다.


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

다음은 full provider object를 활용해 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 {}

'NestJS > Docs_FUNDAMENTALS' 카테고리의 다른 글

Asynchronous Providers  (0) 2024.06.17

+ Recent posts