Backend/NestJS

NestJS - Provider와 Nest IoC Container

hou27 2023. 3. 18. 21:41

왜 그랬는지는 모르겠지만 Typescript Type Challenge를 하다가 갑자기 Provider에 대한 궁금증이 생겼다.

 

Provider

NestJS의 Provider는

Providers are a fundamental concept in Nest.
Many of the basic Nest classes may be treated as a provider – services, repositories, factories, helpers, and so on. The main idea of a provider is that it can be injected as a dependency; this means objects can create various relationships with each other, and the function of "wiring up" instances of objects can largely be delegated to the Nest runtime system.

 

라고 공식문서에 설명되어있다.

 

즉 Provider는 다른 클래스에 의존성으로 주입될 수 있는 항목을 말하며,

services, repositories, factories, helpers 등이 있다.

그리고 객체의 인스턴스를 연결하는 작업은 Nest의 런타임 시스템에 위임된다.

 

물론 NestJS 혹은 Spring을 사용해봤다면 "의존성 주입"이라는 키워드를 접해봤고

위 문장이 밑줄 친 문장이 어떤 의미인지도 바로 알 수 있겠지만

 

와닿지 않는다면 아래 포스트를 읽고 넘어와도 좋다.

Dependency-Injection

 

Dependency Injection(의존성 주입 - DI)란?

NestJS 공부할 때부터 접하던 개념인데 이번에 Spring을 공부하면서 제대로 짚고 넘어가야겠단 생각이 들었다. Dependency Injection 만약 A 객체가 B 객체의 변화에 영향을 받는다면 A는 B를 의존한다고

hou27.tistory.com

 

여기까진 Ok 였다.

 

궁금증

포스트의 시작에서 언급한 궁금증이란,

Provider로 등록하기 위해 작성했던 코드가 어떻게 동작하고,
NestJS에서 의존성 주입을 무엇이 어떻게 진행하는지

였다.

 

방금 말한 등록을 위해 작성한 코드는 무엇일까?

NestJS에서 Provider로 등록하려면 다음과 같은 절차를 밟아야 한다.

  1. provider로 사용할 클래스에 @Injectable 데코레이터를 붙인다.
  2. 해당 Module의 providers 배열에 추가한다.

+ 다른 모듈에서도 사용하기 위해선 exports 배열에 추가하면 된다.

 

예제 코드와 함께 다시 살펴보도록 하자.

 

예제 코드는 공식 문서에서 가져왔다.

Service

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;
  }
}

provider로 등록될 service의 모습이다.

클래스 위에 @Injectable() 데코레이터를 추가함으로써

CatsService가 Provider로 동작할 수 있게 되었다.

 

여기서 끝이 아니다.

 

이렇게만 해두고 아래와 같이

Controller

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) {}

  ...
}

Controller에서 의존성 주입을 받으려 한다면

Nest can't resolve dependencies of the CatController (?).
Please make sure that the argument CatService at index [0] is available in the CatModule context.

위와 같이 에러를 맞이하게 된다.

 

즉 NestJS가 CatController가 의존하는 CatService 클래스가

CatModule Context에서 사용 가능한지 확인할 수 없다는 것이다.

 

NestJS는 Module 중심의 아키텍처를 사용하여 전체 Application을 구성하기 때문에

각 Module 별로 Provider를 캡슐화하고 서로 분리되어 관리된다.

 

그래서 Service에 @Injectable 데코레이터만 추가하고 Controller에서 주입받으려 한다면

지금과 같은 에러가 발생하게 되는 것이다.

 

Module

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 {}

이렇게 @Module 데코레이터를 추가한 Module의

providers 배열에 등록한 Provider(공급자)를 추가해야,

 

비로소 의존성 주입이 에러없이 동작하게 된다.

 

@Injectable

아까 분명 @Injectable 데코레이터를 통해 Service가 Provider로 동작할 수 있게 됐다고 표현했었다.

@Injectable 데코레이터의 정확한 역할과 동작 방식은 무엇일까?

 

간단하게 역할을 표현하자면 크게 2가지로 나눌 수 있다.

  • 해당 클래스를 의존성 주입을 통해 사용할 수 있도록 한다.
  • 해당 클래스가 의존성 주입을 받을 수 있게 한다.

의존성 주입을 통해 사용할 수 있도록 한다

@Injectable 데코레이터는 메타 데이터를 첨부하여

provider가 Nest IoC 컨테이너에서 관리할 수 있는 클래스임을 선언한다.

 

의존성 주입을 통해 사용할 수 있도록 한다는 것은

@Injectable 데코레이터를 붙여둔 클래스를 다른 클래스에서 의존성 주입을 통해

사용할 수 있다는 것이다.

의존성 주입을 받을 수 있게 한다

다른 provider를 주입받아 동작할 수 있도록 한다.

 

그렇다면 의존성 주입을 받는 Controller에는 @Injectable 데코레이터를

붙이지 않는 이유는 무엇일까?

Controller는 @Controller 데코레이터를 붙여서 정의하는데,

@Controller 데코레이터는 내부적으로 @Injectable 데코레이터를 포함하고 있기 때문에

이미 의존성 주입을 받을 수 있는 상태이므로 추가로 붙여줄 필요가 없는 것이다.

 

Nest IoC Container

IoC Container는 Provider를 등록하고 관리하는 객체이다.

Provider는 NestJS의 라이프 사이클과 동기화된 Scope를 가지며

프로그램이 시작될 때 모든 종속성을 처리한다.

Provider는 의존성 주입을 통해 다른 클래스와 관계를 맺을 수 있는데,
IoC Container는 Provider의 메타데이터를 분석하여 의존성 그래프를 생성한다.


IoC Container는 의존성 그래프에 따라 필요한 Provider를 인스턴스화하고 주입하며,

이 과정에서 @Injectable 데코레이터가 사용된다.
또한 인스턴스화된 Provider를 저장하고, 참조할 수 있게 해주는데 이때는 @Inject 데코레이터가 사용된다.


즉, IoC Container는 Provider를 컨테이너에 등록하고, 필요할 때마다 Provider 인스턴스를 생성하여

생성과 관리를 개발자가 아닌 프레임워크가 수행하는 역할을 하게 된다.

 

정리

정리하자면,

@Injectable 데코레이터를 통해 의존성 주입을 받을 수도, 주입이 될 수도 있게 하며,

NestJS의 특성상 모듈에 등록하기 위해 @Module 데코레이터의 providers에 Provider를 추가해야 한다.

 

이런 Provider들을 관리하는 역할은 Nest IoC Container가 수행하며,

이런 도움 덕에 코드의 재사용성과 유지보수성을 높일 수 있게 된다.

 


참고자료

 

Providers

NestJS DI Fundamentals