NestJS 帶你飛! 시리즈 번역 06# Provider (상)

2023. 5. 31. 16:43개발 문서 번역/NestJS

728x90
이 포스팅은 「NestJS 기초실무 가이드 : 강력하고 쉬운 Node.js 웹 프레임워크로 웹사이트 만들기」
(서명: NestJS 基礎必學實務指南:使用強大且易擴展的 Node.js 框架打造網頁應用程式)
책이 출간되었습니다. 

 

전의 포스팅에서 Provider와 Module 사이에 의존성 주입 이라는 중요한 개념이 있다고 설명 드렸습니다.

이번 포스팅에서는 먼저 의존성 주입과 Nest가 어떻게 컴파일링 과정을 거치는지에 대해 설명하고

다시 Provider를 어떻게 사용하는지 알려드리겠습니다.

배움의 과정에선 항상 물음표의 연속이겠지만 매일매일 꾸준히 학습하다 보면 눈에 띄는 발전이 있을겁니다.
그럼 시작 하겠습니다.

 

의존성 주입(Dependency Injection)

의존성 주입은 획기적으로 클래스간의 결합도를 낮추고 유연성을 증가시키는 하나의 프로그래밍 개념입니다.

간단하게 두개의 class를 생성하고 그 둘을 ComputerCPU라고 명명한 후 예를 들어보겠습니다. 

class CPU {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

class Computer {
  cpu: CPU;
  constructor(cpu: CPU) {
    this.cpu = cpu;
  }
}

Couputer의 구조를 보면 CPU가 파라미터로써 들어가 있는걸 확인할 수 있습니다.
이렇게 사용하게되면 CPU의 기능은 CPU에만 존재하여 Computer
CPU의 기능을 조작할 필요가 없어진다는 장점이 있습니다.

다른 CPU를 할당할 때도 엄청 편리해집니다.

const i7 = new CPU('i7-11375H');
const i9 = new CPU('i9-10885H');
const PC1 = new Computer(i7);
const PC2 = new Computer(i9);

 

Nest의 의존성 주입 원리

하지만 의존성 주입과 Provider, Module은 대체 무슨 관계가 있는걸까요?

그 답은 바로..

우리가 Controller에서 constructor에 Service를 주입한 후 어떠한 new도 쓰지 않고 바로 썼었다는걸 알게됩니다.

아래는 app.controller.ts의 예제입니다.

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

인스턴스화 없이 이 인스턴스들은 대체 어디서 생긴걸까요?
앞에서 Module이 생성되는 동시에 providers안의 항목들이 인스턴스화 된다고 언급한적이 있습니다.
우리가 주입했던 Service는 이런 메커니즘을 통해 생성되며 인스턴스가 생성 된것입니다.

다시말해 이 인스턴스들을 관리하는 시스템이 있다는 뜻이며

이 관리하는 시스템을 IoC 컨테이너 (IoC Container, 控制反轉容器)라고 합니다.

이 IoC 컨테이너는 token으로 원하는 항목을 찾아내는데요. 이는 일종의 key/value의 개념과 비슷합니다.

여기서 의문이 하나 생깁니다.
우리가 token을 지정하지도 만들지도 않았는데 Nest는 대체 어떻게 내가 원하는 인스턴스가 어떤것인지 아는걸까요? 

사실, 우리가 providers를 작성할 때 이미 지정하였습니다.

아래는 app.module.ts의 예제입니다. 

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [
    AppService
  ],
})
export class AppModule {}

AppService를 썼을 뿐인데 token을 지정했다구요...? 맞습니다.

사실 AppService 자체는 줄여쓴거고 이를 펼쳐보면 아래와 같은 모습입니다.

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [
    { provide: AppService, useClass: AppService }
  ],
})
export class AppModule {}

하나의 객체가 된 모습을 확인할 수 있습니다.
이 객체의 providetoken을 의미하며 useClass는 사용할 class를 지정하며

위의 사항을 토대로 인스턴스를 생성합니다.

 

Provider

Provider는 IoC 컨테이너를 통해 인스턴스를 생성하며 관리하고 Provider를 편리하고 효율적으로 사용할 수 있습니다.

Provider는 대체로 두가지 유형으로 나뉩니다.

 

표준 Provider

이 방법이 제일 간단하며 대다수의 Service가 쓰고있는 방법이기도 합니다.

class@Injectable 데코레이터를 추가해 Nest에게 이 class는 IoC 컨테이너를 통해 관리할 수 있음을 알려줍니다.

일반적으로 Service는 커맨드를 통해 생성합니다.

$ nest generate service <SERVICE_NAME>
주의: <SERVICE_NAME>은 경로를 포함할 수 있습니다. (ex: features/todo)
경로를 포함하여 생성하게 되면 src 폴더 아래에 경로를 포함한 Service가 생성될 것입니다.

app.service.ts의 예제입니다.

import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

Module안의 providers에 Service를 작성하면 끝납니다.

아래는 app.module.ts의 예제입니다.

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

구문을 풀어쓰고 싶다면(보다 명시적으로 사용하고 싶다면) 아래와 같은 방법으로 작성할 수도 있습니다.

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [
    {
      provide: AppService,
      useClass: AppService
    }
  ],
})
export class AppModule {}

 

 

커스텀 Provider

일반 Provider가 아래의 세 조건을 만족 할때 사용하면 됩니다.

  • Nest가 생성해주는 인스턴스가 아닌 개발자가 인스턴스를 생성하여 사용하고 싶을 때
  • 다른 의존성 항목(클래스)과 인스턴스를 재사용하고 싶은 경우
  • 편리하게 테스트를 진행하기 위해 mock 버전의 class를 재정하고 싶을 경우

Nest는 커스텀 Provider를 만들 수 있는 몇가지의 방법을 제공해줍니다.
모두 명시적으로 정의할 수 있습니다.

 

 

 

Value Provider

이 유형의 Provider는...

  • 상수(Constant)를 제공할 때, 이때 Provide에 들어갈 값을 constant.ts 등에서 미리 정의해두기도 합니다.
  • 외부 라이브러리를 IoC 컨테이너에 주입하고자 할 때
  • class를 특정한 mock 객체로 대체하고자 할 경우

경우에 사용할 수 있는데요. 어떻게 사용해야 할까요?

명시적으로 useValue를 정의하면 됩니다.

아래는 app.module.ts의 예제입니다. 

 

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [
    {
      provide: AppService,
      useValue: {
        name: 'WOO'
      }
    }
  ],
})
export class AppModule {}

app.controller.ts를 수정한 후 AppServicetoken을 확인해 볼까요?

주입했던 AppService가 우리가 지정한 객체로 변해 출력되고 있습니다. 아래는 그 터미널 결과입니다.

{ name: 'WOO' }

 

비 token 유형

사실 Provider의 tokenclass를 쓸 필요는 없습니다. Nest는 아래의 3개 항목도 token으로써 사용할 수 있습니다.

  • string
  • symbol
  • enum

app.module.ts 예제를 하나 작성해보겠습니다.
우리가 지정했던 token을 문자열 HANDSOME_MAN과 값으로써 WOO를 정의하도록 하겠습니다. 

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [
    AppService,
    {
      provide: 'HANDSOME_MAN',
      useValue: 'WOO'
    }
  ],
})
export class AppModule {}

주입을 할 때는 @Inject(token?: string)의 형식인 데코레이터로 값을 불러와야 하는것을 잊지마세요.

아래는 app.controller.ts의 예제입니다.

 

import { Controller, Get, Inject } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(
    private readonly appService: AppService,
    @Inject('HANDSOME_MAN') private readonly handsome_man: string
  ) {
    console.log(this.handsome_man);
  }

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

 

우리가 주입했던 HAND_SOME_MAN가 지정한 값이 터미널에 출력됩니다.

WOO
주의:  다른 곳에서도 이 token이 필요할 때 사용하기 위해 보통 token의 이름은 파일을 따로 하나 생성하여 보관합니다.
물론 해당 파일에 직접 작성해도 되지만 따로 보관하게되면 token 명칭을 한번 더 쓸 필요가 없어집니다.

 

Class Provider

이 유형의 Provider는 가장 고전적인 방법이며
token에 추상 클래스를 지정하고 useClass로 다른 환경에서 다른 클래스를 사용하도록 해보겠습니다. 아래는 app.module.ts의 예제입니다.

 

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TodoModule } from './features/todo/todo.module';
import { TodoService } from './features/todo/todo.service';

class HandSomeMan {
  name = 'WOO';
}

class TestHandSomeMan {
  name = 'WOO';
}

@Module({
  imports: [TodoModule],
  controllers: [AppController],
  providers: [
    AppService,
    {
      provide: TodoService,
      useClass: process.env.NODE_ENV === 'production' ? HandSomeMan : TestHandSomeMan
    }
  ],
})
export class AppModule {}
주의: TodoService를 생성하지 않고 먼저 TodoModule을 생성하고 내보내기를 진행 해야합니다.
먼저 생성해버렸다면 내보내기가 되었는지 꼭 확인해주세요.

app.controller.ts를 조금 수정해보겠습니다.

 

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { TodoService } from './features/todo/todo.service';

@Controller()
export class AppController {
  constructor(
    private readonly appService: AppService,
    private readonly todoService: TodoService
  ) {
    console.log(this.todoService);
  }

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

NODE_ENV의 환경변수가 production이 아니라면 터미널에는 아래의 결과가 출력될 것입니다.

TestHandSomeMan { name: 'WOO' }

 

Factory Provider

이 유형의 Provider는 의존성 주입을 통해 인스턴스를 동적으로 사용하고자 할 때 사용합니다.

무척 중요한 기능이고, useFactory를 통해 팩토리 함수를 정의하며 inject를 통해 다른곳에 의존성을 주입하여 사용합니다.

아래는 app.module.ts의 예제입니다.

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';


class MessageBox {
  message: string;
  constructor(message: string) {
    this.message = message;
  }
}

@Module({
  imports: [],
  controllers: [AppController],
  providers: [
    AppService,
    {
      provide: 'MESSAGE_BOX',
      useFactory: (appService: AppService) => {
        const message = appService.getHello();
        return new MessageBox(message);
      },
      inject: [AppService]
    }
  ],
})
export class AppModule {}

 

app.controller.ts도 살짝 수정해보겠습니다.

import { Controller, Get, Inject } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(
    private readonly appService: AppService,
    @Inject('MESSAGE_BOX') private readonly messageBox
  ) {
    console.log(this.messageBox);
  }

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

아래는 터미널 결과입니다.

MessageBox { message: 'Hello World!' }

 

Alias Provider 

이 Provider는 주로 이미 정의된 Provider에게 별칭을 지어주고자 할 때 사용합니다.

useExist를 통해 어떤 Provider에게 사용할 것인지 정의할 수 있습니다.

아래는 app.module.ts의 예제입니다.

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [
    AppService,
    {
      provide: 'ALIAS_APP_SERVICE',
      useExisting: AppService
    }
  ],
})
export class AppModule {}

이렇게 ALIAS_APP_SERVICEAppService의 인스턴스를 가리키게 할 수 있습니다.

app.controller.ts를 수정해 테스트 해보겠습니다.

import { Controller, Get, Inject } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(
    private readonly appService: AppService,
    @Inject('ALIAS_APP_SERVICE') private readonly alias: AppService
  ) {
    console.log(this.alias === this.appService); // 같은지 비교
  }

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

아래는 터미널의 결과입니다. 같음을 확인할 수 있습니다.

true

마치며

Provider는 매우 중요한 작동 원리 중 하나이며 이 방대한 양을 한 편의 포스팅에 담기란 힘든 일입니다.

남은 부분은 다음 포스팅에서 설명하도록 하겠습니다.

오늘 배운 사항을 간단히 정리하며 포스팅을 마치겠습니다.

 

1. Provider와 Module 사이에는 의존성 주입을 통하여 관계를 만듭니다.

2. IoC 컨테이너를 통해 Provider 인스턴스를 관리합니다.

3. Provider는 표준 Provider, 커스텀 Provider로 나뉩니다.

4. 커스텀 Provider는 축약표기법이 아닌 명시적으로 구문을 표현합니다.

5. Nest는 4개의 커스텀 Provider을 제공합니다. : useValue, useClass, useFactory, useExist

6. Provider의 tokenstring, symbol, enum이 될 수 있습니다.