NestJS Overview - Providers

Min Su Kwon·2021년 10월 17일
0

네스트의 많은 클래스(service, repository, factory, helper 등)가 프로바이더로 취급될 수 있다. 프로바이더의 메인 아이디어는 의존성으로 취급되어 주입될 수 있다는 점이다. 이는 곧 객체들이 서로 다양한 관계를 만들 수 있으며, 객체 인스턴스를 서로 연결하는 작업을 네스트 런타임에게 일임할 수 있다는 뜻이다.

이전 챕터에서, 우리는 간단한 CatsController를 만들었다. 컨트롤러는 HTTP 요청을 핸들링하고, 더 복잡한 태스크는 프로바이더에게 위임한다. 프로바이더는 플레인 자바스크립트 클래스로, 모듈에서 providers 배열에 들어간다.

네스트는 객체지향적인 방법으로 의존성을 설계하고 정리하는 방법을 지원하기 때문에, SOLID 원칙을 따르길 권장한다.

Services

간단한 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;
  }
}

CLI를 통해서 서비스를 만드려면, 커맨드라인에서 nest g service cats 커맨드를 실행한다

CatsService는 하나의 프로퍼티와 두개의 메서드를 가지는 간단한 클래스다. 특이한 점이 하나 있다면, @Injectable() 데코레이터를 사용한다는 점이다. 이 데코레티어는 메타데이터를 붙여주며, CatsService가 네스트 IoC 컨테이너에 의해서 관리될 것이라는 것을 선언한다. 위 예시에서는 Cat 인터페이스도 사용하고 있으며, 아래와 같이 생겼다.

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

고양이들을 조회할 서비스 클래스가 있으니, 이제 CatsController 내부에서 사용해보자

import { Controller, Get, Post, Body } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';

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

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

  @Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll();
  }
}

CatsService는 클래스 생성자에게 주입된다. 위 예시에서는 생성자 시그니쳐에서 private shorthand 문법을 사용하고 있는데, 이렇게 선언해주면 catsService 멤버를 선언함과 동시에 초기화할 수 있다.

Dependency injection

네스트는 의존성 주입이라는 강력한 디자인 패턴을 중심으로 만들어졌다. 이에 관한 훌륭한 Angular Documentation 아티클이 있는데, 이를 읽어보길 권장한다.

타입스크립트의 타입 덕분에, 네스트의 의존성 관리는 매우 쉽다. 네스트는 CatsService 클래스를 통해 인스턴스를 반환함으로써 간단하게 catsService 프로퍼티를 resolve할 수 있다.

constructor(private catsService: CatsService) {}

Scopes

프로바이더는 일반적으로 애플리케이션의 생명주기와 동기화된 생명주기를 가진다. 애플리케이션이 부트스트랩 될 때, 모든 의존성이 resolve되고, 따라서 모든 프로바이더가 인스턴스화된다. 비슷하게, 애플리케이션이 꺼지면, 모든 프로바이더가 파괴된다. 하지만, 프로바이더를 request-scoped 생명주기를 가지도록 하는 방법 또한 있다. 자세한 방법은 다음 문서를 참고한다.

Custom providers

네스트는 빌트인 제어 역전 컨테이너(IoC)를 가지고 있으며, 이 컨테이너가 프로바이더간의 관계를 resolve해준다. 이 기능을 통해서 위에서 설명한 주입 기능을 사용할 수 있는 것이며, 사실 지금까지 설명한 것보다 훨씬 강력한 기능이다. 프로바이더를 정의하는 방법이 여러가지 있다. 플레인 값, 클래스, async/sync 팩토리 모두 사용할 수 있다. 자세한 내용은 다음 문서를 참고한다.

Optional providers

때때로, 반드시 resolve 되어야하는 것은 아닌 의존성이 있을 수 있다. 예를 들면, 클래스가 config 객체에 의존하고 있지만 아무것도 전달되지 않았을 경우 디폴트 값들이 사용되어야 한다. 이 경우에는, 의존성은 optional하게 된다. 왜냐하면 config 프로바이더가 없더라도 에러로 이어지지 않기 때문이다.

프로바이더가 optional하다는걸 표현하려면 @Optional() 데코레이터를 생성자 시그니처에 사용한다.

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

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

위의 예시는 커스텀 프로바이더를 사용하고 있으며, 따라서 HTTP_OPTIONS라는 커스텀 토큰을 사용하고 있다. 커스텀 프로바이더와 토큰의 자세한 내용에 대해서는 다음 문서를 참고한다.

Property-based injection

지금까지 사용한 테크닉은 생성자 기반의 의존성 주입으로, 프로바이더가 생성자 메서드를 통해서 주입된다. 몇몇 경우엔는, 프로퍼티 기반의 의존성 주입이 유용할 수 있다. 예를 들어, 탑 레벨에 있는 클래스가 하나 이상의 프로바이더에 의존하고 있는 경우, 해당 클래스의 서브클래스들이 의존성을 super() 메서드를 통해 전달 받는 것이 싫증날 수 있다. 이를 피하기 위해서, @Inject() 데코레이터를 프로퍼티 레벨에 사용한다.

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

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

클래스가 다른 프로바이더를 extend하고 있지 않다면, 생성자 기반의 의존성 주입이 추천된다

Provider registration

이제 프로바이더(CatsService)를 정의했고, 해당 프로바이더를 사용하는 소비자(CatsController)가 있으니, 네스트에 해당 서비스를 등록해서 의존성 주입이 가능하도록 해줘야한다. 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 {}

이제 네스트가 CatsController 클래스에게 필요한 의존성을 resolve 할 수 있게 되었다.

이제 프로젝트는 위와 같은 구조를 보이게 된다.

Manual Instantiation

지금까지, 네스트가 어떻게 의존성 주입을 핸들링하는지 살펴봤다. 때로는 이 빌트인 의존성 주입 시스템에서 벗어나 직접 프로바이더를 인스턴스화해야 할 수도 있다. 크게 두가지 경우로 나뉜다.

이미 존재하는 인스턴스를 가져오거나 프로바이더를 동적으로 인스턴스화하려면, Module Reference를 사용한다.

bootstrap() 함수 내에서 프로바이더를 가져오려면, Standalone Applications 문서를 읽어봐야한다.

느낀점

프로바이더를 선언한 뒤, 어디서 어떻게 사용할지 명시해주는 과정이 굉장히 명확하다. 어떤 자바스크립트 클래스던지 프로바이더로 사용가능하고, 모듈 쪽에 명시만 해주면 네스트의 의존성 주입에 의해서 간단하게 사용할 수 있게된다. 프로바이더를 사용하는 쪽에서는 어떤 것(타입스크립트의 힘을 빌려서)을 필요로 하는지만 적어놓고, 실제 런타임에서 사용될 인스턴스를 넘기는건 네스트가 해준다.

옵셔널 프로바이더는 아직 사용해본적이 없는데, 성능 관련 최적화가 필요할 때 사용하면 되는걸까? 아직 잘 모르겠다.

Reference

profile
이제 막 커리어를 시작한 소프트웨어 엔지니어입니다. 배운 것을 정리하면서 조금 더 깊이 이해하려는 습관을 들이려고 합니다. 피드백은 언제나 환영입니다.

0개의 댓글