Node.js/NestJS

[NestJS] 3. 프로바이더

턴태 2022. 9. 27. 15:16

프로바이더

프로바이더는 Nest의 근간이 되는 개념입니다. 많은 Nest의 기본 클래스들(services, repositories, factories, helpers 등)이 provider로 취급될 수 있습니다. 프로바이더의 가장 주된 아이디어는 의존성 주입입니다. 의존성 주입은 객체가 서로 다양한 관계를 생성할 수 있다는 점이며, 객체의 인스턴스를 연결하는 기능은 Nest 런타임 시스템에 위임될 수 있습니다.

앞선 컨트롤러 예제처럼, 컨트롤러는 HTTP 요청들을 처리하고, 더 복잡한 일들을 프로바이더에 위임합니다. 프로바이더는 모듈에서 프로바이더로 위임된 일반적인 자바스크립트의 클래스입니다.

 

서비스

간단하게 CatsService를 만들어 보겠습니다. 이 서비스는 데이터 저장소 및 검색의 역할을 하며, CatsController에 사용되기 위해 디자인되었습니다.

 

import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  create(cat: Cat) {
    this.cats.push(cat);
  }

  findAll(): Cat[] {
    return this.cats;
  }
}

여기서 만든 CatsService는 한 개의 프로퍼티와 두 개의 메서드로 이루어진 기본적인 클래스입니다. 여기서 새로운 특징은 @Injectable() 이라는 데코레이터를 사용했다는 것입니다. @Injectable() 데코레이터는 메타데이터를 통해 CatsService가 Nest의 IoC(제어 역전) 컨테이너에 의해 관리되는 클래스라는 것을 선언합니다.

 

여기서 Cat은 어떤 타입인지 모르기 때문에 인터페이스를 만들어주어야 합니다.

export interface Cat {
  name: string;
  age: number;
  breed: string;
}

이제 cats를 검색할 수 있는 서비스 클래스를 갖고 있으니, 컨트롤러에서 이를 활용해봅시다.

import { Controller, Get, Post, Req } from '@nestjs/common';
import { Body, Param, Query, Redirect } from '@nestjs/common/decorators';
import { Request } from 'express';
import { CreateCatDto } from './dto/create-cats.dto';
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();
  }

  @Post()
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }
}

CatsService는 클래스의 생성자 함수를 통해서 주입됩니다. 여기서 주목할 것은 private라는 문법입니다. 이것은 catsService의 멤버가 같은 장소에서 선언 및 생성될 수 있도록 해줍니다. 그래서 Controller에서 자기 참조 변수를 통해서 catsService의 메서드를 사용할 수 있는 것입니다.

 

그러면 Postman으로 HTTP 메서드를 통해 확인해보겠습니다.

 

먼저 POST /cats로 값을 push합니다.

 

그 다음 GET /cats 요청을 보내보면, catsService에서 정의한 메서드대로 정상적으로 값을 출력합니다.

의존성 주입

Nest는 흔히 의존성 주입으로 잘 알려진 강력한 디자인 패턴을 기반으로 구축되었습니다. Nest에서는 타입스크립트의 기능 덕분에 의존성 주입을 관리하기 매우 편리합니다. 왜냐하면, 의존성 주입이 타입에 의해 해결되기 때문입니다.

 

아래의 예처럼 Nest는 CatsService의 인스턴스를 생성하고 반환함으로써 catsService를 해결할 수 있습니다. 이 의존성은 적절히 처리되어 컨트롤러의 생성자 함수로 전달됩니다.

 

스코프

프로바이더는 어플리케이션의 생명주기와 동기화된 스코프라는 라이프 타임을 갖고 있습니다. 어플리케이션이 구동될 때, 모든 의존성을 해결해야 하기 때문에 모든 프로바이더가 인스턴스화 되어야만 합니다. 비슷하게 어플리케이션이 셧다운될 때, 모든 프로바이더는 메모리에서 삭제될 것입니다. 그러나, 프로바이더의 수명을 요청 단위로 만드는 방법이 있습니다. 이것은 향후에 알아보겠습니다.

 

커스텀 프로바이더

Nest는 프로바이더 사이의 관계를 해결하기 위한 제어 역전 컨테이너를 기반으로 구축되었습니다. 이러한 특징은 위에서 설명한 의존성 주입 기능의 기초가 되지만, 사실 앞서 설명한 것보다 훨씬 더 강력합니다.

 

일반적인 값이나, 클래스, 비동기 및 동기 factories들을 사용하여 프로바이더를 정의할 수 있습니다.

 

선택적 프로바이더

때때로, 반드시 해결할 필요가 없는 의존성들을 갖고 있을 수 있습니다. 예를 들어, 클래스가 configuration 객체를 기반으로 만들어졌지만, 아무 값도 넣지 않았을 때 기본 값이 사용되는 경우입니다. 이러한 경우에서 의존성은 선택사항이 됩니다. 왜냐하면 configuration 프로바이더가 없더라도 에러가 발생하지 않을 것이기 때문입니다.

 

프로바이더가 선택사항임을 알리기 위해서는 생성자 시그니처에 있는 @Optional() 데코레이터를 사용합니다.

 

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

@Injectable()
export class HttpService<T> {
  constructor(@Optional() @Inject('HTTP_OPTIONS') private httpClient: T) {}
}

프로퍼티 기반 주입

앞서 사용한 기술은 생성자 기반 주입으로, 프로바이더는 생성자 메서드를 통해 주입됩니다. 매우 특별한 경우에, 프로퍼티 기반 주입이 유용할 수도 있습니다. 예를 들어, 만약 최상위 클래스가 한개 이상의 프로바이더에 의존한다면, 모든 자식 클래스에서 생성자 함수로부터 super를 호출하도록 처리하는 것은 매우 힘든 일이 될 겁니다. 이 문제를 방지하기 위해 @Inject() 데코레이터를 프로퍼티를 정의하는 위치에 사용합니다.

 

프로바이더 등록

CatsService라는 프로바이더를 정의했고, 이 서비스를 사용하는 CatsController도 정리했기 때문에, 이제 이 서비스를 Nest에 등록하여 의존성을 수행할 수 있도록 해야 합니다. 이것은 app.module.ts 파일에서 수행하고 서비스를 @Module() 데코레이터에 있는 providers 배열에 추가함으로써 할 수 있습니다.

 

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 {}
src
├ cats
│	├ dto
│	│	└ create-cat.dto.ts
│	├ interfaces
│	│	└ cat.interface.ts
│	├ cats.controller.ts
│	└ cats.service.ts
├ app.module.ts
└ main.ts