Nest js Provider

Nest JS 를 배우며

Nest JS에 대해 공부를 하며 정리하는 글.

계층형 구조(Layered Architecture)

소프트웨어 업계에서는 계층형 구조라는 기법을 사용한다.

복잡해 보이는 작업도 그 작업을 나누고 각 작업마다 역량을 집중하면 쉽게 해결할 수 있다.

MVC에서 관심사를 분리하여 유지보수 및 개발을 쉽게 만드는 원리와 같다고 본다.

몇 개의 계층으로 구분하는냐에 따라서 다르지만 보통 3계층 구조를 많이 사용한다.

Presentation Tier

사용자 인터페이스 혹은 외부와의 통신을 담당.

Application Tier

Logic Tier라고 하기도 하고 Middle Tier라고 하기도 한다. 주로 비즈니스 로직을 여기서 구현을 하며, Presentation Tier와 Data Tier사이를 연결해준다.

Data Tier

데이터베이스에 데이터를 읽고 쓰는 역할을 담당한다.

Nest에서의 Presentation Tier는 외부의 입력을 받아들이는 컨트롤러이며 서비스는 Application Tier에 해당한다.

서비스에는 주로 비즈니스 로직이 들어간다.

Nest는 이렇게 컨트롤러와 그 하위 계층을 프로바이더라는 이름으로 구분하며, 이는 응집도는 높이고 결합도는 낮추는 소프트웨어 설계이다.


제어 역전 (IoC, Inversion of Control)

스포 : 개발자가 제어하던 것을 프레임워크가 제어하게 된다고 하여 제어의 역전이라고 부름.

제어 역전을 한 마디로 표현한다면 “나 대신 프레임워크가 제어한다” 라고 표현할 수 있다.

제어 역전을 설명하기 위해서는 의존성이라는 개념을 알아야한다.

타입스크립트를 비롯한 많은 언어에서는 클래스를 사용하려면 new 같은 키워드로 인스턴스화를 시켜야 한다.

붕어빵(인스턴스화시킨 클래스)을 먹지, 붕어빵틀(클래스)을 먹지는 않는 것과 같다.

const sword = new Sword();

Warrior클래스에서 Sword클래스를 인스턴스화 했다.

기획팀에서 다음 업데이트때 이제 전사는 칼 뿐만 아니라 몽둥이도 사용할 수 있다고 지령이 내려왔다.

좋은 객체지향 설계는 구체적인 개념에 의존하지 말고 추상적 개념에 의존해야 한다.

위 코드에서 new를 사용하면 SwordSword를 생성하는 Warrior 사이에 의존성이 생긴다.

정확히는 WarriorSword에 의존하게 된다.

이렇게 직접적이고 구체적으로 클래스를 인스턴스화하는 행동은 바람직하지 않다.

이럴때 인터페이스가 사용된다. 프로그래밍에서 인터페이스는 규약이다.

인터페이스를 구현하려면 인터페이스가 원하는 규약을 따라야 한다.

반대 급부로 프로그래머는 내가 호출하는 클래스가 무엇인지 정확하게 알 필요가 없다.

다만 특정 기능이 동작 가능하다는 사실만 알고 개발하면 된다.

interface Weaponable {
  swing(): void;
}

interface Playable {
  attack(): void;
}

class Warrior implements Playable {
  private Weaponable weapon;

  constructor Warrior(private readonly Weaponable _weapon) {
    weapon = _weapon;
  }

  public void attack() {
    weapon.swing();
  }
}

class Mongdungee implements Weaponable {
  public void swing() {
    console.log('Mongdungee Swing!');
  }
}

몽둥이 쥔 전사 클래스를 인스턴스화 시켜보자.

Warrior warrior = new Warrior(new Mongdungee());

하지만 클래스 계층 구조가 복잡한 프로그램에서 직접 저 몽둥이를 넘겨야 한다거나, 여러 전사에게 같은 몽둥이를 넘기거나 한다는 상황에서는 그닥 좋지 않다.

이 경우 제어 역전을 사용한다.

Nest는 제어 역전을 추상화해서 그 동작이 잘 보이지 않기 때문에 원 글에서는 typedi를 사용해서 이를 구현했다.

import "reflect-metadata";
import { Container, Service } from "typedi";

// Weaponable, Playble 은 위와 같음

@Service()
class Mongdungee implements Weaponable {
  public void swing() {
    console.log('Mongdungee Swing!');
  }
}

@Service()
class Warrior implements Playable {
  // 아래 코드 중요!
  constructor(private readonly weapon: Weaponable) {}

  public void attack() {
    this.weapon.swing();
  }
}


const playerInstance = Container.get<Warrior>(Warrior);
playerInstance.attack();  // "Mongdungee Swing!"

코드 어디에서도 new가 없다. 하지만 잘 동작함을 확인할 수 있다.

이는 typediContainer라는 친구가 알아서 클래스의 인스턴스를 생성했기 때문이다.

이처럼 제어권을 내가 아닌 프레임워크에게 넘기는 것이 제어 역전이다.

여기서 라이브러리와 프레임워크의 결정적인 차이가 발생한다.

라이브러리는 내가 짠 코드에서 필요할 때 라이브러리를 실행시킨다.

즉 라이브러리는 내 코드의 소비자가 될 수 없다. 반면 프레임워크는 내 코드의 소비자가 될 수 있다.

프레임워크는 내가 짠 코드가 필요할 때 알아서 실행시키게 된다.

의존성 주입

제어 역전(이하 IoC)은 나 대신 프레임워크가 제어한다라면 의존성 주입(이하 DI)은 프레임워크가 주체가 되어 네가 필요한 클래스 등을 너 대신 내가 관리해준다는 개념이라고 생각하면 된다.

멀리서 보면 DI보다 IoC가 더 크고 추상적인 개념이다.

IoC는 추상적이기 때문에 이를 구현한게 바로 DI이며 이는 제어 역전의 구현체 중 하나이다.

그래서 DI를 통해 IoC를 구현했다는 말이 나온다. Nest는 DI를 통해 IoC를 구현한 프레임워크이다.

Nest js 공식 Document

프로바이더는 Nest의 기본 개념입니다. 많은 기본 Nest 클래스는 서비스(Service), 레파지토리, 팩토리, 헬퍼 등등의 프로바이더로 취급될 수 있습니다. 프로바이더의 주요 아이디어는 의존성을 주입할 수 있다는 점입니다. 이 뜻은 객체가 서로 다양한 관계를 만들 수 있다는 것을 의미합니다. 그리고 객체의 인스턴스를 연결해주는 기능은 Nest 런타입 시스템에 위임될 수 있습니다.

WarriorMongdungee가 바로 프로바이더이다.

어떤 컴포넌트가 필요하며 의존성을 주입당하는 객체를 프로바이더라고 생각하면 된다.

그리고 Nest 프레임워크 내부에서 알아서 컨테이너를 만들어서 관리해준다는 말이다.

의존성을 주입하기 위한 방법은 크게 3가지가 있다.

  • 생성자를 이용한 의존성 주입(Constructor Injection)

  • 수정자를 이용한 의존성 주입(Setter Injection)

  • 필드를 이용한 의존성 주입(Field Injection)

Nest에서는 주로 생성자를 이용한 의존성 주입을 권장한다.

필드를 이용한 의존성 주입은 한 두 스크롤 아래 속성 기반 주입(Property-based injection)이라는 항목으로 소개한다.


Scope

프로바이더는 일반적으로 Nest 프로그램의 수명 주기와 동기화 된 수명(범위)을 갖는다.

Nest 프로그램이 부트 스트랩 될 때 모든 종속성을 해결해야 하기 때문에 모든 프로바이더가 인스턴스화 된다.

마찬가지로 Nest 프로그램이 종료되면 각 프로바이더가 메모리에서 삭제된다.

그러나 프로바이더의 수명을 요청 단위로 제한하는 방법도 있다.

다만 성능에 문제가 될 수 있기 때문에 특수한 상황이 아니라면 기본 설정된 수명 주기를 사용하는게 좋다.


선택적 프로바이더 (Optional Provider)

때때로, 반드시 해결될 필요가 없는 종속성이 있을 수 있다.

예를 들어 클래스는 configuration 객체에 의존할 수 있지만 해당 인스턴스가 없는 경우 기본값을 사용하는 경우이다.

이러한 경우 에러가 발생하지 않으므로 종속성이 선택사항이 된다.

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

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

프로바이더가 선택적임을 나타내려면 constructor 서명에 @Optional() 데코레이터를 사용하면 된다.


속성 기반 주입(Property-based Injection)

앞서 설명한 의존성 주입의 3가지 방법 중 3번째 방법인 "필드를 이용한 의존성 주입"이다.

지금까지 예제는 의존성이 생성자 방법을 통해 주입되기 때문에 생성자를 이용한 의존성 주입이라고 불린다.

매우 구체적인 경우 속성 기반 주입이 유용할 수 있다.

예를 들어 최상위 클래스가 하나 또는 여러 프로바이더에 종속되어 있는 경우, 생성자에서 하위 클래스의 super()를 호출하여 해당 클래스를 끝까지 전달하는 것은 매우 지루할 수 있다.

이 문제를 방지하려면 속성 수준에서 @Inject() 데코레이터를 사용할 수 있다.

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

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

만약 클래스가 다른 프로바이더를 확장(extend)하지 않는 이상 반드시 생성자를 이용한 의존성 주입을 사용하는 것이 좋다고 한다.

Last updated