HJW's IT Blog

NestJS Document 알아보기 : Providers 본문

NestJS

NestJS Document 알아보기 : Providers

kiki1875 2024. 10. 19. 18:30

 

Provider

NestJS 에서 Provider는 의존성 주입 시스템의 핵심 개념중 하나이다. Provider 는 Nest 어플리케이션 내에서 인스턴스를 관리하고 주입하는 역할을 담당한다. Provider 는 어떤 특정한 것을 가르키기 보단, Nest 에서 Service, Repository, Factory function, value 혹은 기타 의존성을 제공할 수 있는 클래스나 값이다.

즉 Provider 의 주요 개념은 의존성을 주입할 수 있다 는 것이다.

이를 통해 객체들이 서로 다양한 관계를 만들 수 있으며, 이러한 객체들을 “연결” 하는 작업을 개발자는 Nest의 런타임 시스템에 맡길 수 있다.

이전에 설명했던 Controller는 HTTP 요청을 처리하고 복잡한 작업은 Providers 에게 위임해야 한다.

NestJS 에서 Provider는 @Injectable() 데코레이터를 사용해 정의된다.

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

@Injectable()
export class MyService {
  // 서비스 로직
}

이후, 의존성 주입을 통해 다른 클래스에 주입시켜야 한다. NestJS 는 자동으로 각 클래스의 생성자에 필요한 의존성을 주입해 주기 때문에, 개발자는 직접 인스턴스를 생성하지 않아도 된다.

import { Injectable } from '@nestjs/common';
import { MyService } from './my.service';

@Controller()
export class MyController {
  constructor(private readonly myService: MyService) {}

  someMethod() {
    this.myService.someServiceMethod();
  }
}

모듈 내에서도 등록을 해주어야 한다. NestJS에서는 Provider를 모듈 내에서 등록하여 해당 모듈 내에서 자유롭게 사용 가능하다.

import { Module } from '@nestjs/common';
import { MyService } from './my.service';
import { MyController } from './my.controller';

@Module({
  providers: [MyService],
  controllers: [MyController],
})
export class MyModule {}

 

💡 NestJS 는 객체 지향적인 방식으로 의존성을 설계하고 조작하기 때문에, SOLID 원칙을 따르는 것을 강력하게 권장한다.

 

Document Example

이전 포스트에서 생성한 CatController 와 CatInterface를 가져오겠다.

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();
  }
}
export interface Cat {
  name: string;
  age: number;
  breed: string;
}

 

이제 CatController 가 사용할 CatService를 생성해야 한다.

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

@Injectable()
export class CatService {

	private readonly cats: Cat[] = [];
	
	create(cat: Cat){
		this.cats.push(cat);
	}
	
	findAll(): Cat[]{
		return this.cats;
	}
}

 

Injectable 데코레이터는 CatService 가 Nest 의 IoC 컨테이너에 의해 관리될 수 있는 클래스라는 메타 데이터를 추가한다.

이때, CatService 는 생성자 주입을 통해 주입이 되는데, 이때 private 구문을 사용하면, catService 멤버를 한번에 선언 및 초기화가 가능하다.

constructor(private catsService: CatsService) {}

// private 선언하지 않을 경우

catService : CatService
constructor(catsService: CatsService) {
	this.catService = catService;
}

 

Typescript 의 기능 덕분에 Nest 는 의존성을 쉽게 관리할 수 있다. Type 만으로 의존성이 해결되기 때문이다. 의존성을 수동으로 관리할 필요가 없으며, 타입 안정성이 증가한다. NestJS는 클래스의 생성자에서 요구하는 의존성의 타입을 분석하여 타입의 Provider 를 찾아 인스턴스를 주입한다.

다음은 NestJS에서 Controller에 의존성이 주입되는 과정이다.

  1. 컨트롤러 정의
  2. 의존성 선언
  3. 모듈 등록
  4. 의존성 해결 : 어플리케이션 시작시 모듈을 로드하고 의존성 관계를 분석한다
  5. 인스턴스 생성 : IoC 컨테이너가 인스턴스를 생성한다
  6. 의존성 주입: 컨트롤러가 인스턴스화 될 때, NestJS는 생성자에 인스턴스를 자동으로 주입한다.
    1. 기본적으로 주입되는 서비스 인스턴스는 싱글톤으로 관리된다.

 

Scope

Provider는 기본적으로 어플리케이션 라이프사이클과 동기화된 수명(scope) 를 가진다. 어플리케이션이 bootstrap 될 때, 모든 의존성이 해결되어야 하며, 따라서 모든 Provider 가 인스턴스화 된다. 하지만 Provider 의 수명을 요청 단위로 변경할 수도 있다. 이에 대해서는 추후 다루도록 하겠다.

Optional Providers

때로는 해결될 필요가 없는 의존성이 있을 수 있다. 예를 들어 클래스가 설정 객체에 의존하지만 설정이 전달되지 않으면, 기본값을 사용하는 경우가 있다. 이럴때 의존성은 선택사항이 되며, 설정 Provider가 없더라도 오류가 발생하지 않는다.

이를 명시하기 위해 @Optional 데코레이터를 사용할 수 있다.

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

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

Property-based Injection

지금까지의 모든 예시는 생성자 주입 방식으로, 생성자를 통해 provider를 주입하는 방식이었다. 하지만 특정 상황에서는 Property based injection 이 유용할 수 있다. 예를 들어 상위 클래스가 여러 제공자에 의존하는 경우, 생성자에서 super()를 호출하여 모든 제공자를 하위 클래스로 전달하는 것이 번거로울 수 있다. 이를 피하기위해, @Injectable() 을 프로퍼티 수준에서 사용할 수 있다.

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

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

하지만 어디까지나 ‘가능하다’ 의 범주인것이지, 클래스가 다른 클래스를 상속하지 않는다면 생성자 기반 주입을 사용하는것이 권장된다.

@Injectable()
class ParentService {
  @Inject(DependencyService)
  protected readonly dependencyService: DependencyService;

  someMethod() {
    this.dependencyService.doSomething();
  }
}

@Injectable()
class ChildService extends ParentService {
  // 부모 클래스의 dependencyService를 그대로 사용 가능
  
  @Inject(AnotherDependency)
  private readonly anotherDependency: AnotherDependency;

  newMethod() {
    // 부모의 의존성 사용
    this.dependencyService.doSomething();
    
    // 자식 클래스의 추가 의존성 사용
    this.anotherDependency.doSomethingElse();
  }
}

Provider Registration (제공자 등록)

Provider 를 정의했고, 이를 사용하는 controller를 정의한 후에는, NestJS 에 서비스를 등록해야 한다. 이를 위해 모듈 파일을 수정해야 한다.

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

Manual Instantiation (수동 인스턴스화)

지금까지는 Nest가 대부분의 의존성 해결을 자동으로 처리하는 방식에 대해 설명했다. 하지만 특정 상황에서는 빌트인 의존성 주입 시스템을 벗어나 수동으로 제공자를 가져오거나 인스턴스화해야 할 수 있다. 이러한 두 가지 주제를 간략히 살펴보자.

  1. Module reference를 사용하여 기존 인스턴스를 가져오거나 제공자를 동적으로 인스턴스화할 수 있다.
  2. bootstrap() 함수 내에서 제공자를 가져오고 싶을 경우(예: 컨트롤러가 없는 독립형 애플리케이션에서 또는 부트스트래핑 중에 설정 서비스를 사용하기 위해) 이는 독립형 애플리케이션에서 논의된다.