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

자바스크립트 비동기 처리 3가지 방식

JS는 싱글 스레드 프로그래밍 언어기에 멀티 작업을 하기 위해서는 비동기 처리 방식이 자주 쓰인다.

비동기 처리는 백그라운드로 동작되기에 결과가 언제 반환될지 알 수 없어, 완료되면 결과를 받아 처리하기 위해 대표적으로 Callback 함수나, Promise 객체를 사용한다. 하지만 서비스 규모가 커질 수록 코드가 복잡해짐에 따라 코드를 중첩하여 사용하다가 가독성이 떨어지고 유지보수가 어려워지는 상황이 발생한다. 이를 Callback Hell, Promise Hell이라고 부른다.

/* Callback Hell */
getData (function (x) {
  getMoreData (x, function (y) {
    getMoreData (y, function (z) {
      ...
    });
  });
});
/* Promise Hell */
fetch('https://example.com/api')
  .then(response => response.json())
  .then(data => fetch(`https://example.com/api/${data.id}`))
  .then(response => response.json())
  .then(data => fetch(`https://example.com/api/${data.id}/details`))
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error(error));

위의 코드를 보면 Callback hell과, then 핸들러의 남용은 구현하고자하는 의도의 파악과 가독성이 크게 떨어진다.

JS의 async와 await는 이런 문제들을 해결하기 위해 탄생했으며, 가독성과 유지보수성을 향상시켜준다.

async function getData() {
    const response = await fetch('https://example.com/api');
    const data = await response.json();
    const response2 = await fetch(`https://example.com/api/${data.id}`);
    const data2 = await response2.json();
    const response3 = await fetch(`https://example.com/api/${data.id}/details`);
    const data3 = await response3.json();
    console.log(data3);
}

getData();

위 코드를 보면 마치 함수의 리턴값을 변수가 받은 정의문 형식대로 되어 있어 코드가 의도하고자 하는 바를 동일 코드 레벨 라인에서 아수가 있어 편하다.

JS async/await

async/await는 ES2017에 도입된 문법으로, Promise 로직을 더 쉽고 간결하게 사용할 수 있게 해준다. async/await가 Promise를 대체하기 위한 기능이 아니라는 것이다. 내부적으로는 여전히 Promise를 사용해서 비동기를 처리하고, 단지 코드 작성 부분을 프로그래머가 유지보수학 편하게 보이는 문법만 다르게 해줄 뿐이라는 것이다.

async/ await 기본 사용법

async/await는 절차적 언어에서 작성하는 코드와 같이 사용법도 간단하고 이해하기 쉽다. function 키워드 앞에 async만 붙여주면 되고, 비동기로 처리되는 부분 앞에 await만 붙여주면 된다.

다음은 setTimeout 비동기 함수를 이용해 delay 기능을 구현한 프로미스 객체 비동기 함수를 기존 Promise.then()방식과 async/await 방식으로 똑같이 처리하지만 다르게 코드를 구현한 예제이다.

// 프로미스 객체 반환 함수
function delay(ms) {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log(`${ms} 밀리초가 지났습니다.`);
      resolve()
    }, ms);
  });
}

// 기존 Promise.then() 형식
function main() {
  delay(1000)
      .then(() => {
        return delay(2000);
      })
      .then(() => {
        return Promise.resolve('끝');
      })
      .then(result => {
        console.log(result);
      });
}

// 메인 함수 호출
main();

// async/await 방식
async function main() {
  await delay(1000);
  await delay(2000);
  const result = await Promise.resolve('끝');
  console.log(result);
}

// 메인 함수 호출
main();

예시 코드를 보면 promise는 then 메서드를 연속적으로 사용하여 비동기 처리를 한다.
async/await는 await 키워드로 비동기 처리를 기다리고 있다는 것을 직관적으로 표현하고 있음을 볼 수 있다.

async/await의 장점을 비동기적 접근방식을 동기적으로 작성할 수 있게 해주어 코드가 간결해지며 가독성을 높여져 유지보수를 용이하게 해준다.

async 키워드

await를 사용하기 위한 선언문이다. 즉 function앞에 async를 붙여줌으로써, 함수 내에서 await 키워드를 사용할 수 있게 된다.

// 함수 선언식
async function func1() {
    const res = await fetch(url); // 요청을 기다림
    const data = await res.json(); // 응답을 JSON으로 파싱
}
func1();

// 함수 표현식
const func2 = async () => {
    const res = await fetch(url); // 요청을 기다림
    const data = await res.json(); // 응답을 JSON으로 파싱
}
func2();

async 리턴값은 Promise 객체

async fuction에서 어떤 값을 리턴하든 무조건 promise 객체로 감싸져 반환된다.

async function func1() {
  return 1;
}

const data = func1();
console.log(data); // 프로미스 객체가 반환된다

다른 Promise 상태를 반환하기

직접 Promise 정적 메서드를 통해 다음과 같이 Promise 상태(state)를 다르게 지정하여 반환이 가능하다.

async function resolveP() {
  return Promise.resolve(2);
}

async function rejectP() {
  return Promise.reject(2);
}

reject 같은 경우 위와 같이 Promise.reject() 정적 메서드를 통해 반환되는 Promise 상태를 실패(rejected) 상태로 지정해줄 수있지만, async 함수 내부에서 예외 throw를 해도 실패 상태의 Promise 객체가 반환된다.

async function errorFunc() {
    throw new Error("프로미스 reject 발생시킴");
}

만일 async fuction에서 일부러 return을 하지 않아도 자동으로 return undefined으로 처리되게 때문에 어찌됬든 무조건 Promise 객체를 반환하게 된다.

async 함수와 then 핸들러

Promise 객체를 반환하기에 then 핸들러를 붙일 수 있다. 허나 then 핸들러를 남용할 경우 Promise Hell에 걸릴 수 있기에 대신 await 사용을 권장한다.

async function func1() {
  return 1;
}

func1()
    .then(data => console.log(data));

await 키워드

await 키워드는 promise.then() 보다 좀 더 세련되게 비동기 처리의 결과값을 얻을 수 있도록 해주는 문법이다.

예를 들어, 서버에 리소르를 요청하는 fetch() 비동기 함수를 다음과 같이 then 핸들러 방식으로 결과를 얻어 사용해왔을 것이다.

// then 핸들러 방식
fetch(url)
    .then(res => res.json()) // 응답을 JSON으로 파싱
    .then(data => {
      // data 처리
      console.log(data);
    })

await 키워드를 사용하면 then 핸들러를 복잡하게 처리할 필요 없이, 심플하게 비동기 함수 왼쪽에 await만 명시해주고 결과값을 변수에 받도록 코드를 정의하면 끝이다. then과 콜백 함수를 남발하여 코드가 들여쓰기로 깊어지는 것을 방지하고, 한 줄 레벨에서 코드를 나열하여 가독성을 높일 수 있다.

// await 방식
async function func() {
    const res = await fetch(url); // 요청을 기다림
    const data = await res.json(); // 응답을 JSON으로 파싱
    // data 처리
    console.log(data);
}
func();

await는 Promise 처리가 끝날때까지 기다림

await는 Promise 비동기 처리가 완료될때 까지 코드 실행을 일시 중지하고 wait한다. 예를 들어 fetch() 함수를 사용하여 서버에서 데이터를 가져오는 경우를 생각해보자. 이 함수는 Promise를 반환한다. 따라서 await 키워드를 사용하여 이 Promise가 처리될 때까지 코드 실행을 일시 중지하고, Promise가 처리되면 결과 값을 반환하여 변수에 할당하는 식이다.

async function getData() {
  const response = await fetch('https://jsonplaceholder.typicode.com/users/1');
  const data = await response.json();
  console.log(data):
}

await는 Promise를 처리하고 결과를 반환하는 데, 비동기적인 작업을 동기적으로 처리할 수 있게 해준다.

async/await 에러 처리

기존의 Promise.then() 방식의 에러 처리는 catch() 핸들러를 중간 중간에 명시함으로써 에러를 받아야 했다.

// then 핸들러 방식
function fetchResource(url) {
  fetch(url)
    .then(res => res.json()) // 응답을 JSON으로 파싱
    .then(data => {
      // data 처리
      console.log(data);
    })
    .catch(err => {
      // 에러 처리
      console.error(err);
    });
}

async/await의 경우 try/catch를 통해서 에러를 처리한다.

// async/await 방식
async function func() {

    try {
        const res = await fetch(url); // 요청을 기다림
        const data = await res.json(); // 응답을 JSON으로 파싱
        // data 처리
        console.log(data);
    } catch (err) {
        // 에러 처리
        console.error(err);
    }

}
func();

async/await 함정과 병렬처리

비동기 프로그래밍은 웹에서 메인 스레드를 차단하지 않고 시간 소모적인 작업을 병렬적으로 수행할 수있도록 하는 웹 개발의 필수적인 부분이다.
JS에서 비동기 코드를 처리하는 문법으로 Promise, await 키워드를 사용하는 것 이다. 그러나 await를 남발하면 성능 문제 및 기타 문제가 발생할 수 있다. await는 Promise가 해결될 때까지 함수 실행을 일시 중지 하는 것인데, 병렬적으로 멀티로 처리할 수 있는 작업을 억지로 동기적으로 처리하게 함으로써 오히려 2초만에 해결할 로직을 6초씩이나 걸리게 할 수 있기 때문이다.

따라서 await를 올바르게 사용하지 않으면 오히러 성능 문제가 발생하기에, JS await 남용에 대한 몇 가지 주의 사항과 Promise.all을 통해 병렬 처리를 최적화하는 방법을 살펴볼 예정이다.

적절하지 않은 async/await 사용

function getApple(){
  return new Promise( (resolve, reject) => {
    setTimeout(() => resolve("apple"), 1000);
  })
}

function getBanana(){
  return new Promise( (resolve, reject) => {
    setTimeout(() => resolve("banana"), 1000);
  })
}

async function getFruites(){
  let a = await getApple(); 
  let b = await getBanana(); 
  console.log(`${a} and ${b}`);
}

getFruites();

위의 코드에서 apple과 banana는 1초가 걸린다. 만일 이를 병렬적으로 처리하면 console에 찍히기 까지 1초가걸리지만, 동기적으로 처리하면 2초가 걸린다. 위의 비동기함수 getApple, getBanana는 서로 연관이 없다. 그럼에서 생각없이 await 키워드를 두 번 붙이면 동기적으로 실행되는데, 이는 명백한 시간 낭비이다.

async function getFruites(){
  console.time();
  let a = await getApple(); // 1초 소요
  let b = await getBanana(); // 1초 소요
  console.log(`${a} and ${b}`);
  console.timeEnd();
}

getFruites();

적절한 async/await 사용

핵심은 Promise 객체 함수를 await와 같이 써서 실행시키는 것이 아니라, 미리 함수를 동기/논블록킹으로 실행하고 그 결과 Promise값을 await를 통해 받는 식이다.

위의 코드 처럼 getApple(), getBanana() 비동기 로직이 만일 순서를 지켜야하는 로직이라면 위와 같이 구성하는게 옳지만, 현재느 서로 연관이 없기에 반드시 순차적으로 실행 시킬 필요가 없다. 따라서 비동기 처리 요청과 값을 await하는 로직을 분리시키면 된다.

async function getFruites(){

  // 미리 실행시킴
  let getApplePromise = getApple(); // async함수를 미리 논블록킹으로 실행한다. 
  let getBananaPromise = getBanana(); // async함수를 미리 논블록킹으로 실행한다. 

  // 이렇게 하면 각각 백단에서 독립적으로 거의 동시에 실행되게 된다.
  console.log(getApplePromise)
  console.log(getBananaPromise)

  let a = await getApplePromise; // 위에서 받은 프로미스객체 결과 변수를 await을 통해 꺼낸다.
  let b = await getBananaPromise; // 위에서 받은 프로미스객체 결과 변수를 await을 통해 꺼낸다.

  console.log(`${a} and ${b}`); // 본래라면 1초+1초 를 기다려야 하는데, 위에서 1초기다리는 함수를 바로 연속으로 비동기로 불려왔기 때문에, 대충 1.01초만 기다리면 처리된다.
})

Promise.all

또 다른 방법으로는 Promise.all() 정적 메서드를 사용하는 방법이 있다. 위와 같이 구성할 경우 비동기 처리 완료 시점을 가늠하기 힘들기에 대부분 Promise.all()로 처리한다

Promise.all()은 배열 인자의 각 Promise 비동기 함수들이 모두 resolve되어야 결과를 리턴 받는다. 배열 인자의 각 promise 함수들은 제각각 비동기 논블로킹으로 실행되어 시간을 단축할 수 있다. return 값은 각 promise 함수의 반환값들이 배열로 담겨져 있다.

async function getFruites(){
  console.time();

  // 구조 분해로 각 프로미스 리턴값들을 변수에 담는다.
  let [ a, b ] = await Promise.all([getApple(), getBanana()]); 
  console.log(`${a} and ${b}`);

  console.timeEnd();
}

getFruites();

출처

https://inpa.tistory.com/entry/JS-%F0%9F%93%9A-%EB%B9%84%EB%8F%99%EA%B8%B0%EC%B2%98%EB%A6%AC-async-await

'JS' 카테고리의 다른 글

동기, 비동기  (1) 2024.06.12
Call back 함수  (1) 2024.06.11
Binding의 개념과 call, apply, bind  (1) 2024.06.08

+ Recent posts