[NestJS] Provider (프로바이더)

Devhslee·2023년 10월 15일

NestJS 기초

목록 보기
5/6

NestJS에서 DI의 핵심은 Module과 Provider이다.

앞서 NestJS에서 Provider의 의존성 주입을 관리하는 주체가 Module이라고 했었다. 연관된 provider들은 module로 캡슐화되어 이곳저곳에 주입되어 사용된다. 사실상 NestJS 프로젝트의 코드의 90프로는 provider로 취급되는 녀석들이라고 보면 된다.
(후술하겠지만, Pipe나 Guard, 심지어 소켓 통신을 위한 Gateway 역시도 provider이다)

특정 클래스를 다른 곳에서 주입이 가능한 객체로 만들려면 @Injectable 데코레이터로 지정해준다. @Injectable 데코레이터로 지정된 클래스는 NestJS의 IoC 컨테이너가 의존성 관계와 생명주기를 관리해 준다.


인스턴스 생성이 아닌 의존성 주입

NestJS에서 module, provider를 비롯한 거의 모든 클래스는 singleton이다.
따라서 EmailService라는 provider를 UserService에서 사용하고자 할 때, (UserModule에서 이미 EmailService를 export하는 module를 import했다고 가정)

다음과 같이 코드를 작성하는 게 아니라,

import { injectable } from '@nestjs/common';
import { EmailService } from './email/email.service';

@Injectable()
export const UserService {
  private emailService: EmailService;
 
  constructor() { this.emailService = new EmailService(); }
}

아래와 같이 의존성을 주입해서 사용하는 것이다. (JAVA의 Spring도 아래와 같은 방식을 사용한다)

import { injectable } from '@nestjs/common';
import { EmailService } from './email/email.service';

@Injectable()
export const UserService {
  constructor(private readonly emailService: EmailService) {}
}

private으로 지정해 주는 이유는 주입받는 해당 provider를 곧바로 member로서 초기화하기 위해서이다.


provider의 생명주기 (Injection scope)

특별히 지정해 주지 않는 이상 모든 provider의 생명주기는 곧 애플리케이션의 생명주기나 다름없다. 즉, 애플리케이션이 부팅되면 모든 모듈들과 연관된 provider들의 의존성이 연결되고, 꺼지면 provider들의 인스턴스도 파괴되는 것이다.

만약 특정 provider가 어떤 request가 들어올 때마다 인스턴스화되고 다시 사라져야 한다면 어떻게 해야 할까? 공식 문서에서는 provider의 생명주기를 Injection scope 라고 표현하고 있다.

Injection scope는 크게 세 가지가 있다.


Default scope(=application lifetime)
provider의 singleton 인스턴스가 애플리케이션 전역에서 공유되는 것이다.
앞서 말했듯이 애플리케이션이 부팅되면 provider의 singleton 인스턴스가 만들어지고, 애플리케이션이 꺼지면 그 인스턴스는 사라지는 것이다.


Request scope
provider가 singleton으로 인스턴스화되는 것이 아닌, 들어오는 요청마다 그때그때 인스턴스를 만들고 요청이 끝나면 사라지는 것이다 (자동으로 garbage-collecting된다). 단, 동일한 HTTP 요청 내에서는 인스턴스가 공유된다.

Request scope로 provider를 지정하고 싶다면 다음과 같이 범위를 지정해주면 된다.

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

@Injectable({ scope: Scope.REQUEST })
export class UserService {}

Transient scope
Request scope와 마찬가지로 자신의 singleton 인스턴스가 공유되지는 않는다. 해당 provider를 사용하려고 호출할 때마다 인스턴스를 생성한다.

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

@Injectable({ scope: Scope.TRANSIENT })
export class UserService {}

얼핏 보면 Request scope나 Transient scope나 별 차이가 없어 보인다. 둘 다 요청마다 새로운 인스턴스를 사용한다는 특징이 있다.

구체적인 차이가 있다면 요청 간에 인스턴스를 공유하는가 이다.

UserController에서 UserService를 주입받고 있다고 치자.

import { Controller, Get, Query } from '@nestjs/common';
import { Request, Response } from 'express';
import { UserService } from './user.service';

@Controller('user')
export class UserController {
  constructor(private userSerivce: UserService) {}

  @Get()
  async getUser(@Query('id') id: number) {
    const user = await this.userService.checkUserExist(id);
    const userInfo = await this.userService.findUserDetail(user.id);
    return userInfo;
  }
}

위에서 보다시피 UserService의 메서드는 2번 호출되고 있는 상황이다.

이때, UserService의 scope가 뭔지에 따라 인스턴스가 몇 번 만들어지는지 달라진다.

Request scope인 경우
getUser()라는 같은 HTTP 요청 내에서 UserService의 메서드를 호출하고 있다. 이 경우 checkUserExist()를 호출할 때와 findUserDetail()을 호출할 때 같은 인스턴스를 공유한다.

Request scope인 경우
같은 요청 내에서 호출되었는지와 상관없이 그냥 불릴 때마다 인스턴스를 생성하고 없앤다.
그 말인즉슨 checkUserExist()를 호출할 때와 findUserDetail()을 호출할 때의 인스턴스가 서로 다른 것이라는 뜻이다.


그렇지만 그냥 이런 식으로 개발자가 임의로 Injection scope를 지정해줄 수 있구나, 하고만 알아두고 어지간한 이상 그냥 singleton instance를 사용하는 것이 낫다. (공식문서 권장) 그렇게 생성된 인스턴스의 캐싱과 초기화가 단 한 번만 일어나면 그만이기 때문이다.

Optional Provider

위에서 썼던 코드를 다시 들고 와보자.

import { injectable } from '@nestjs/common';
import { EmailService } from './email/email.service';

@Injectable()
export const UserService {
  constructor(private readonly emailService: EmailService) {}
}

이렇게 되면 애플리케이션이 bootstrap될 때 UserService이 EmailService의 의존성을 주입하는게 강제된다.

만약에, 특정 상황에만 UserService가 EmailService의 의존성을 주입하여 사용해야 하는 경우가 있다면 어떨까? 다시 말하면 특정 상황에서 UserService로 EmailService가 아닌 none이 들어올 경우 말이다. 이런 경우 아무런 조치도 취해주지 않는다면 당연히 bootstrap 될 때 에러를 뿜을 것이다.

반드시 의존성을 주입하지 않아도 되는 provider라면 앞에 @Optional 를 붙여준다.

import { injectable, Optional } from '@nestjs/common';
import { EmailService } from './email/email.service';

@Injectable()
export const UserService {
  constructor(@Optional() private readonly emailService: EmailService) {}
}

custom provider?

여기까지 봤을 때 크게 어려운 건 없다.
다만 위에서 든 예시들은 모두 정적으로 provider를 주입해준 것이다. 다시 말해, EmailService를 주입한다면 말 그대로 EmailService를 사용한다는 것이지 다른 타입을 들고 오지는 않는다는 것이다.

그럼 EmailService를 주입할 때 상황에 따라 다른 클래스를 주입할 수도 있다는 뜻일까? 충분히 있을 수 있는 일이다.

공식 문서에 이와 관련하여 custom providers 항목에서 자세히 설명하고 있다. 다음 포스팅은 이걸 정리해 보도록 하겠다.

profile
코딩-버그-좌절-해결-희열

0개의 댓글