[server] Nest.js 프로바이더

정종훈·2022년 6월 29일
0

providers

개요

참조: https://jakekwak.gitbook.io/nestjs/overview/untitled-4 —1

참조: https://wikidocs.net/158499 —2

컨트롤러는 요청과 응답을 가공하고 처리하는 역할을 맡는다고 했음

그러나 서버가 제공하는 핵심기능은 전달받은 데이터를 어떻게 비즈니스 로직으로 해결하는가임.

만약 음식 배달 앱에서 메뉴 목록 조회를 요청했다고 했을 때, 사용자 주변에 위치한 가게를 DB에서 검색하는 작업을 수행해야 합니다. 또 사용자가 좋아할 만한 메뉴가 학습되어 있다고 하면 이를 기반으로 추천 메뉴 구성을 바꿀 수도 있을 것입니다.

앱이 제공하고자 하는 핵심 기능, 즉 비즈니스 로직을 수행하는 역할을 하는 것이 프로바이더입니다.

컨트롤러가 이 역할을 수행할 수도 있으나 단일 책임 원칙에 의해 분리해주는게 좋을듯 싶다.

프로바이더는 서비스, 레포지토리, 팩토리, 헬퍼등 여러가지 형태로 구현이 가능

Nest에서 제공하는 프로바이더의 핵심은 의존성 을 주입할 수 있다는 것!

예시) 컨트롤러와 프로바이더의 관계

// users.controller.ts
@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}
    ...

    @Delete(':id')
    remove(@Param('id') id: string) {
      return this.usersService.remove(+id);
    }
}

컨트롤러는 비즈니스 로직을 직접 수행하지 않음.

컨트롤러에 연결된 UsersService에서 수행함!

UsersService는 UsersController의 생성자(컨스트럭터)에서 주입받아, usersService라는 객체 멤버 변수에 할당되어 사용되고 있습니다.

그럼 UsersService파일은 어떨까?

// users.service.ts

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

@Injectable()
export class UsersService {
    ...

  remove(id: number) {
    return `This action removes a #${id} user`;
  }
}

@Injectable 데코레이터를 주목하자.

이 데코레이터를 UsersService 클래스에 선언함으로써

다른 어떤 Nest 컴포넌트에서도 주입할 수 있는 프로바이더가 됨!

별도의 스코프를 지정해 주지 않으면 일반적으로 싱글톤 인스턴스가 생성됨

공부) 싱글톤 인스턴스

잠깐의 이론공부

참조: https://www.wisewiredbooks.com/nestjs/overview/04-provider.html

계층형 구조

관심사 분리랑 비슷.

예전에 배웠던 3-Tier Architecture 와 비슷함.

표현계층은 컨트롤러이며

애플리케이션 계층은 서비스(비즈니스 로직)

데이터 티어는 SQL, graphQL 이런것들

제어 역전

나 대신 프레임워크가 제어한다임.

우선 의존성 이라는 개념을 잠깐 보자.

예를들어

const sword = new Sword();

이것은 Sword클래스를 인스턴스화 한거임

예를들어 Warrior 클래스에서 sword 클래스를 인스턴스화해서 막 가져다 썼어

근데 sword 클래스가 업데이트 된거임.

그러면 SwordSword를 생성하는 Warrior 사이에 의존성이 생긴거 정확히는 Warrior 가 종속된 것이지.

이렇게 직접적이고 구체적으로 클래스를 인스턴스화하는 행동과 불편함을 해소하기 위해 인터페이스 라는게 나타난것!

예시) 제어 역전 어려움…

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());

임 (뭔진 이해못함)

이것을 제어 역전 을 추상화해서 동작이 잘 보이지 않게 하면

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) 는 프레임워크가 주체가 되어 네가 필요한 클래스 등을 너 대신 내가 관리해준다는 개념

IoC와 헷갈리기는 하지만 DI가 조금 더 깊고 구체적인 개념

정리

그래서 결론은

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

프로바이더 등록과 사용

프로바이더 등록

참조: https://wikidocs.net/148511

app.module.ts에 등록해주면 된다.

속성 기반 주입

아까(예시: 컨트롤러와 프로바이더의 관계)는 생성자를 통해 프로바이더를 주입받았다.

그러나 직접 주입받지 않고 상속관계에 있는 자식 클래스를 주입받아 사용하고 싶은경우가 있다.

예를들어 레거시 클래스를 확장한 새로운 클래스를 만드는 경우 새로 만든 클래스를 프로바이더로 제공하고 싶은 경우.

이럴 때는 자식 클래스에서 부모 클래스가 제공하는 함수를 호출하기 위해서 부모 클래스에서 필요한 프로

바이더를 super()를 통해 전달 해주어야 함.

예제) 어질어질한 서비스들

// /service/app.parentService.ts
import { ServiceA } from './app.serviceA';

export class BaseService {
  constructor(private readonly serviceA: ServiceA) {}

  getHello(): string {
    return 'Hello World BASE!';
  }

  doSomeFuncFromA(): string {
    return this.serviceA.getHello();
  }
}

// /service/app.serviceA.ts

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

@Injectable()
export class ServiceA {
  getHello(): string {
    return 'Hello World A!';
  }
}

// /service/childService.ts

import { Injectable } from '@nestjs/common';
import { BaseService } from './app.parentService'

@Injectable()
export class ServiceB extends BaseService {
  getHello(): string {
    return this.doSomeFuncFromA();
  }
}

// /controller/app.controller.ts

@Controller()
export class AppController {
  constructor(
    private readonly serviceB: ServiceB,
  ) { }

  @Get('/serviceB')
  getHelloC(): string {
    return this.serviceB.getHello();
  }
}

공부) 현재 부모는 BaseService이고 getHello() 와 doSomeFuncFromA() 메서드 2개가 있다.

또한 자식은 ServiceB로 getHello() 메서드를 가지고 있다.

컨트롤러에서 serviceB를 주입받고, getHello()를 호출한다면

이는 BaseService의 doSomeFuncFromA 함수를 호출하게 됨.

그러나 부모의 BaseService는 주입을 받을 수 있는 클래스로 선언되어 있지 않기 때문에

Nest의 IoC 컨테이너는 생성자에 선언된 ServiceA를 주입하지 않습니다.

그래서 Nest는 ServiceA의 존재를 모르고 에러가 날거임

이상태에서

curl http://localhost:4000/serviceB

을 실행하게 되면 500에러가 발생함

부모에서 getHello() 를 찾을 수 없단다.

이를 해결하기 위해서 ServiceB에서 super를 통해 ServiceA의 인스턴스를 전달해 주어야함!

// /service/childService.ts

@Injectable()
export class ServiceB extends BaseService {
 /* constructor(private readonly _serviceA: ServiceA) { 
    super(_serviceA); 
  } 
*/

  getHello(): string {
    return this.doSomeFuncFromA();
  }
}


아 근데 이제 문제는

최상위 클래스가 하나 또는 여러 프로바이더에 종속되어있는 경우 생성자에서 하위 클래스의 super()를 계속 호출하여 해당 클래스를 끝까지 전달하는 것은 매우 힘들일 like 프롭스 드릴링!

이문제를 해결하기 위해 속성 수준에서 @Inject() 데코레이터를 사용할 수 있음!

// 부모서비스.ts
export class BaseService {
  @Inject(ServiceA) private readonly serviceA: ServiceA;
    ...

    doSomeFuncFromA(): string {
    return this.serviceA.getHello();
  }
}

근데 웬만하면

상속관계에 있지 않는 경우는 속성 기반 주입말고 생성자 기반 주입을 사용해라

profile
괴발개발자에서 개발자로 향해보자

0개의 댓글