Provider

Provider는 Nest의 가장 근본적인 개념이다. 많은 basic한 Nest class들은 provider(service, repository, factory, helper...)로 다루어진다. Provider의 주된 개념은 dependency로서 Provider 주입(injected) 되는 것이다. NestJs에서 객체들은 다양한 관계를 형성할 수 있으며, 이러한 객체들을 "연결(wiring up)"하는 기능은 대부분 Nest 런타임 시스템에 위임될 수 있다.

provider: NestJS에서 Provider는 주입 가능한 서비스, 클래스, 값 또는 기타 의존성으로, 의존성 주입 시스템의 핵심 요소이다. 
제공자는 주로 서비스의 형태로 사용되며, 다양한 구성 요소 간에 재사용 가능한 로직을 캡슐화 한다.

Controller들은 HTTP request들을 다루고, 또 providers에게 복잡한 일들을 위임한다. Providers는 일반적인 JS class이다. 이는 module안에서 provider로서 선언된다.

NestJS는 객체 지향적으로 설계 및 의존성을 조작할 수 있는 기능을 제공하므로, SOLID 원칙을 따르는 것을 권장한다.



Services

Service는 보통 data의 storage, retrieval에 책임이 있다. 그리고 이는 Controller에 의해 사용된다.

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 command을 통해 쉽게 만들 수 있다.

CatService는 간단한 class, 하나의 property와 두 개의 method이다. 그리고 @Injectable() decorator을 사용한다. 이는 NestJS의 종속성 주입 시스템에서 클래스를 관리할 수 있게 해주는 메타데이터를 클래스에 부착한다.

@Injectable(): 이는 NestJs에서 Provider를 정의할 때 사용된다. 이 decorator는 class가 의존성 주입 시스템에 의해 관리될 수 있도록 지정한다.
    @Injectable() 데코레이터가 있는 클래스는 다른 클래스에 주입될 수 있으며, NestJs가 이를 관리하여 필요한 곳에 자동으로 주입한다.(IoC)

일단은, Cat interface를 사용하기에 아래와 같이 작성한다.

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

이후 작성한 CatsService를 CatController에 가져온다.

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는 class constructor에 의해 injected 되었다. 여기서 private 문법을 사용하였다. 이 shorthand는 catsService의 선언과 초기화를 동시에 해준다.

IoC(Inversion of Control)

Don't call us. We'll call you - Hollywood Principle


프레임워크를 적용하지 않은 경우를 생각해보자.
객체의 생명 주기 즉, 객체의 생성, 초기화, 소멸, 메서드 호출 등등을 클라이언트 구현 객체가 직접 관리한다. 또한 다른 사람이 작성한 외부 코드(라이브러리)를 호출하더라도 해당 코드의 호출 시험 역시 직접 사람이 관리한다.


하지만 NestJs와 같은 프레임워크를 사용할 때는 Controller, Service 같은 객체들의 동작을 직접 구현하기는 하지만, 해당 객체들이 어느 시점에서 호출될 지는 신경쓰지 않는다. 프레임워크가 요구하는대로 객체를 생성하면, 프레임워크가 해당 객체들을 가져다가 생성하고, 메서드를 호출하고 소멸시킨다. 즉 프로그램의 제어권이 역전된 것이다.


라이브러리를 사용하는 어플리케이션은 제어 흐름을 라이브러리에 내주지 않는다. 단지 필요한 시점에 라이브러리에 작성된 객체를 적재적소에 가져다 쓸 뿐이다. 하지만 프레임워크를 사용한 어플리케이션의 경우, 어플리케이션 코드에 작성한 객체들을 프레임워크가 필요한 시점에 가져다가 프로그램을 구동하기에 프로그램의 제어권이 프레임워크로 역전된다.


즉 IoC란, 메소드나 객체의 호출작업을 개발자가 결정하는 것이 아니라, 외부의 프레임워크나 라이브러리가 제어 흐름을 결정되는 것을 의미한다. 객체의 의존성을 역전시켜 객체 간의 결합도를 줄이고 유연한 코드를 작성할 수 있게 하여 가독성 및 코드 중복, 유집 보수를 편하게 할 수 있게 한다.


기존에는 다음과 순서로 객체가 만들어지고 실행되었다.

  1. 객체 생성
  2. 의존성 객체 생성: 클래스 내부에서 생성
  3. 의존성 객체 메소드 호출

하지만 Spring, Nest에서는 다음과 같은 순서로 객체가 만들어지고 실행된다.

  1. 객체 생성
  2. 의존성 객체 주입: 스스로 만드는 것이 아니라 제어권을 프레임워크에게 위임하여 프레임워크가 만들어놓은 객체를 주입한다
  3. 의존성 객체 메소드 호출

Nest가 모든 의존성 객체를 Nest가 실행될 때 다 만들어주고 필요한 곳에 주입시켜준다.


class B{
    //
}


class A{
  // 클래스 A가 클래스 B를 사용하기에 A는 B에 의존적
  // 그렇기에 B가 변하면 A도 변해야하는, 즉 영향을 미치는 관계 A -> Bf
    new b = new B()
}

이번에는 NestJs에서의 IoC를 살펴보자


import {Controller, Get} from '@nestjs/common'
import {AppService} from './app.service'

export class AppController{
    // 1. 사용하고 싶은 서비스 타입 객체를 미리 선언한다.
      private appService: AppService
      //  2. 생성자에서 실제로 사용할 서비스 객체를 직접 생성(binding)한다. 
    constructor(){
        this.appService = new AppService();
    }
  ...
}

위의 코드에서는 개발자가 사용하고 싶은 객체가 있으면 이것을 개발자가 생성부터 소멸까지 직접 관리해야 했다.
이렇게 하면 AppService가 변경이 되었을 때 개발자도 그에 맞춰 코드를 수정해야 한다.
IoC를 활용하면 객체의 생명주기 관리 자체를 외부에 위임한다.(이번의 경우 Nest.js IoC 컨테이너에 위임)


IoC는 모듈 간 결합도를 낮추기에 하나의 모듈이 변경되어도 다른 모듈들에는 영향을 최소화되어 웹 어플리케이션을 지속 가능하고 확장성 있게 해준다.


  • 라이브러리
    • 제어권이 나에게 있다.
    • 내 코드가 필요할 때마다 내가 사용하고 싶은 라이브러리를 사용한다.
  • 프레임워크
    • 제어권이 프레임워크에 있다.
    • 나의 코드를 프레임워크가 필요로 하는 경우에 프레임워크가 알아서 실행시킨다.

DI는 이러한 IoC를 수행하는 하나의 방법이며 Nest.js에서는 생성자를 통한 DI를 가장 기본적인 IoC테크닉으로 생각하고 있다.


    constructor(private readonly appService: AppService){} 

추가적으로 SOLID 원칙의 DIP(의존 역전 원칙) 를 같이 보면 확실히 더 이해가 될 것이다.



DI(Dependency injection)

Nest는 Dependency Injection으로 알려진 design pattern을 사용한다. Argular doumentation 참고


NestJS에서는 Typescript의 기능 덕분에 의존성을 매우 쉽게 관리할 수 있다. 의존성은 타입에 의해 자동으로 해결된다. 아래의 예제에서는 NestJS가 CatService의 인스턴스를 생성하여 catsService로 주입한다. 만약 이미 다른 곳에서 요청된 적이 있다면 기존의 싱글톤 인스턴스를 반환한다. 이러한 의존성은 컨트롤러의 생성자에 전달되거나 지정된 속성에 할당된다.

constructor(private catsService: CatsService) {}


Scopes

Provider은 application의 lifecycle과 동기화 되어있는 수명을 가진다. 이는 application이 bootstrap되었을 때, 모든 dependency가 해결되고, 즉 모든 provider는 인스턴스화 된다. 유사하게 만일 application이 shut down 될 때, 각 provider는 파괴된다. 하지만 provider의 lifetime을 요청 단위(request-scoped) 로 만들 수 있다.
https://docs.nestjs.com/fundamentals/injection-scopes

bootstrap: 어플리케이션을 시작하고 초기화하는 과정을 의미한다. 어플리케이션이 부트스트립되면, 모든 의존성이 해결되고 모든 제공자가 인스턴스화되어야 한다.
provider: NestJs의 어플리케이션에서 의존성을 주입할 수 있는 기본 구성 요소이다. 
    service, repository, factory, helper 등과 같은 클래스들이 provider로 사용된다.
singleton:  클래스의 인스턴스가 어플리케이션 전체에서 하나만 존재하도록 보장하는 디자인 패턴


Custom Providers

Nest는 Provider들의 관계를 정리해주는 IoC container를 가진다. 이 특징은 dependency injection feature의 근간이 된다. 하지만 지금까지 서술했던 것 보다 더 중요한게 있다. provider를 정의하는 몇 가지 방법이 있다. 기본적으로 클래스, 값, 또는 asynchronous 및 synchronous factories를 사용할 수 있다.
https://docs.nestjs.com/fundamentals/dependency-injection



Optional providers

때때로, 반드시 해결되지 않아도 되는 dependency가 있을 수 있다. 예를 들어, 클래스가 configuration object에 의존하지만, configuration object가 제공되지 않으면 기본 값을 사용해야 하는 경우가 있다. 이런 경우에, dependency가 optional 될 수 있다.
왜냐하면 configuration provider의 lack은 error로 취급되지 않기 때문이다.

provider가 optional임을 나타내기 위해, @Optional() decorator를 사용할 수 있다.

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

@Injectable()
export class SomeService {
  constructor(@Optional() private readonly configService?: ConfigService) {}

  getConfig() {
    if (this.configService) {
      return this.configService.getConfig();
    }
    return 'default config';
  }
}
//예2
import { Injectable, Optional, Inject } from '@nestjs/common';

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

위의 코드(예2)에서는 HTTP_OPTIONS custom Token을 포함하기에 custom provider를 사용한다. 이전 예제들은 클래스의 생성자에서 constructor-base injection을 통해 dependency를 나타냈다.
https://docs.nestjs.com/fundamentals/custom-providers

custom provider: 기본 제공 provider 외에 개발자가 직접 정의한 provider
token: provider를 식별하는 데 사용되는 고유한 값이다. 문자열, simbol, class를 토큰으로 사용할 수 있다.


Property-based injection

지금까지는 constructor-based의 기술이였다. class의 constructor method를 통해 provider들이 주입되는 거였다. 이번에 서술할 property-based injection도 유용할 것이다. 예를 들어, top-level의 class가 하나의 혹은 여러개의 provider에 의존한다면,
sub-class들의 constructor의 super() 를 사용할 것인데 이는 매우 tedious하다. 대신에 @Inject() decorator를 사용할 수 있다.

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

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

주의사항
만일 class가 다른 class를 extend 하지 않는다면, constructor-based injection을 사용해야 한다. constructor는 명백하게 무슨 dependency들이 필요한지 나타내고, @Inject() 로 나타내는 것보다 더 나은 가시성을 제공한다.



Provider registration

provider CatsService, 그리고 이러한 service를 사용하는 CatsController를 가지고 있다. 그리고 이들을 injection을 위해 Nest에 등록해야한다. 이들 module file인 app.module.ts 통해 추가해야한다. 이들을 providers의 array를 @Module() decorator에 추가한다.

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

Nest는 이제 CatsController class의 의존성을 해결할 것이다.

현재 파일 구조는 다음과 같다.

src
    cats
            dto
                create-cat.dto.ts
            interface
                cat.interface.ts
            cats.controller.ts
            cats.service.ts
    app.module.ts
    main.ts

Manual Instantiation

지금까지, 어떻게 Nest에서 자동적으로 dependency들을 처리하는지를 다뤄왔다. 특정 상황에서는 built-in Dependency Injection system(어플리케이션의 클래스 간 의존성을 자동으로 관리하고 주입하는 메커니즘, IoC 컨테이너)에서 벗어나 수동으로 provider를 검색하거나 인스턴스화해야 할 수도 있다. 아래는 두 가지 주제를 간단히 설명한다.

기존의 instance, instance화가 된 provider를 얻기 위해서는 Module Reference를 사용할수 있다.(ModuleRef)
https://docs.nestjs.com/fundamentals/module-ref

import { ModuleRef } from '@nestjs/core';

@Injectable()
export class MyService {
  constructor(private readonly moduleRef: ModuleRef) {}

  async getDynamicProvider() {
    const provider = await this.moduleRef.create(SomeProvider);
    return provider;
  }
}

bootstrap() 함수에서 providers를 얻기 위해서는 Standalone application을 살펴 봐야한다.
https://docs.nestjs.com/standalone-applications

'NestJS > Docs_OVERVIEW' 카테고리의 다른 글

Exception filters  (0) 2024.06.05
Middleware  (1) 2024.06.04
Modules  (1) 2024.06.04
Controllers  (0) 2024.06.03
First Steps  (0) 2024.06.03

+ Recent posts