본문으로 바로가기

Dependency Injection (DI)?

Nest를 다루다 보면 'Dependency Injection'(DI)가 참 많이 나온다. 아니, Nest에서의 핵심이라고 하는 편이 맞을 것 같다.

DI는 고유한 것이 아니라 JAVA spring, daggar 등 다양한 언어, 프레임워크에서 사용되는 패턴이다.

 

js가 OOP에 적합한 언어가 아니라는 인식이 있어서 DI는 외면당해왔는데, 견고한 node 프로젝트를 위해서는 DI를 사용하는 편이 좋다고 한다. - 견고한 node 프로젝트 설계하기 참고

 

'의존성 주입'이란 용어를 짚어보자.

 

  • 의존성 : 어떤 함수, 클래스 등이 내부에 다른 함수, 클래스를 사용함
  • 주입 : 어떤 함수, 클래스 등이 내부에 사용하는 다른 함수, 클래스를 내부에서 생성하는 것이 아니라 외부에서 생성하여 넣어주는 것을 '주입'이라 함 

말로 풀어보니 감이 올듯 말듯합니다. TS 기반 코드로 개념을 이해해봅시다.

 

 

의존성이란 무엇인가?

아래 Player 클래스는 Weapon 클래스에 대해 의존성을 띄고 있습니다.

Player는 Weapon 클래스를 사용하고 있으니까요.

class Weapon {
  public attack() {
    console.log('hit!');
  }
}

class Player {
  private weapon: Weapon;

  constructor() {
    this.weapon = new Weapon();
  }

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

const me = new Player();
me.attack();

 

그런데 여기서 Weapon 클래스가 아니라 이를 상속 받은 Whip(채찍)이라는 무기를 사용하고 싶다고 가정합니다.

그렇게 되면 Weapon을 의존성으로 가진 클래스를 찾아 바꿔주는 작업이 필요합니다.

이것이 의존성의 단점입니다. 의존하고 있는 코드를 찾아서 전부 바꿔줘야 하는 거죠.

class Weapon {
  public attack() {
    console.log('hit!');
  }
}

class Whip extends Weapon {
  public attack() {
    console.log('whipwhip!');
  }
}

class Player {
  private weapon: Weapon;

  constructor() {
    this.weapon = new Whip();
  }

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

const me = new Player();
me.attack();

 

지금은 Player 클래스 하나 뿐이었지만 만약 Weapon에 의존성을 가진 클래스가 50개 정도 되었다면 죽을 맛일 겁니다.

단순히 수정에서 그치는 것이 아니라 연관된 추가 메서드들, 그리고 해당 클래스를 사용하는 외부의 코드까지도 전부 수정해야하는 참사가 일어날 수도 있습니다.

 

이런 문제를 해결하는 좋은 방법 중 하나가 의존성 주입(Dependency Injection)입니다.

 

 

의존성 주입은 어케하는 거임? + 의존 관계의 역전

어려울 거 없이, 의존하고 있는 코드를 클래스에서 생성자로 받아오면 됩니다.

전자는 의존성을 그대로 사용하는 클래스이며 후자는 의존성을 주입하는 클래스입니다.

 

 

따라서, 아래처럼 사용할 수 있게 되고, 의존성이 변경되어도 내부 로직을 별도로 수정할 필요가 없게 되었습니다.

const me = new Player(new Whip());

 

여기서 우리는, Weapon이 아니라, 의존성을 사용하게 되는 Player 쪽에서 결정하게 되는, 관계가 역전된 모습을 확인해볼 수 있습니다.

이를 "의존 관계의 역전"이라고 부릅니다. 

 

 

 

제어권의 역전 : IoC(Inversion of control)

위의 코드의 경우 우리가 직접 의존성의 인스턴스를 만들어서 주입해주었습니다. 아래처럼요.

const me = new Player(new Whip());

 

이런 방식의 단점이 뭐가 있을까요? 한 클래스가 가질 수 있는 종속성의 양은 매우 많을 겁니다.

우리가 작성한 Player의 경우 Weapon, Armor, Inventory 등등... 많은 종속성이 존재할겁니다.

이를 위해서 새로운 인스턴스를 직접 생성하겠죠. 이런 단순 작업을 실수하기에도 쉽고 번거롭습니다.

 

그런데 IoC 컨테이너(외부)에서 객체를 생성해서 주입한다면, 위와 같이 직접 인스턴스를 생성하지 않아도 됩니다.

인스턴스를 생성하고 관리하는 제어권이 코더인 '우리'가 아니라, typeDI가 같습니다. 이를 제어권의 역전이라고 부릅니다.

 

아리까리하죵? JAVA Spring에서 좋은 설명이 있어 첨부합니다

https://www.youtube.com/watch?v=fGOU7JqNHyE&ab_channel=%EC%9D%B8%ED%94%84%EB%9F%B0-%EC%A7%80%EC%8B%9D%EC%9D%84%EB%B0%B0%EC%9A%B0%EA%B3%A0%EB%82%98%EB%88%A0%EC%9A%94

 

Nest 공식 문서에도 Provider 간의 관계를 관리하는 ("IoC") 컨테이너가 있다고 안내하고 있습니다.

Nest has a built-in inversion of control ("IoC") container that resolves relationships between providers.

출처 - docs.nestjs.com/providers#custom-providers

 

이해가 안 가는데, 코드로 좀 해보자 : typedi

github : github.com/typestack/typedi

docs : github.com/typestack/typedi/blob/develop/docs/SUMMARY.md

(이 녀석들이 왜 내용 추가를 안하는지 모르겠다... 스스로 코드 까보라는 식인가?)

 

inversify 같은 것도 있던데, typedi가 간단하다고 하니 이걸 좀 써보겠습니다.

일반 js 환경에서도 사용할 수는 있는데 제한이 있다고 합니다.

거, 이름부터 type 붙었는데 그냥 타입스크립트 합시다!

 

설치 및 tsconfig를 아래와 같이 구성합니다.

여기서, 데코레이터와 reflect-metadata에 대해서는 제가 작성한 다음 포스트를 참고해주시길 바랍니다.

darrengwon.tistory.com/1128

 

@Decorator (3) : reflect-metadata

typescript-kr.github.io/pages/decorators.html - 메타데이터 (Metadata) 부분을 참고합시다. www.npmjs.com/package/reflect-metadata reflect metadata에 들어가기에 앞서... 왜 쓰냐?는 질문에 runtime reflec..

darrengwon.tistory.com

 

yarn add typedi reflect-metadata

 

자, 아래 코드는 typedi의 예시로 나온 코드입니다. 이것을 좀 뜯어봅시다.

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

@Service()
class ExampleInjectedService {
  printMessage() {
    console.log('I am alive!');
  }
}

@Service()
class ExampleService {
  // ExampleInjectedService에 @Service() 데코레이터를 붙여서
  // typedi가 ExampleService에 ExampleInjectedService의 인스턴스를 자동으로 넣어줌
  constructor(public injectedService: ExampleInjectedService) {}
}

// new로 여기서 직접 생성하는 것이 아니라 Container에서 땡겨오는 식으로 사용함
const serviceInstance = Container.get(ExampleService);

serviceInstance.injectedService.printMessage(); // I am alive!

 

음, 그러니까 인스턴스로 만들 class에는 @Service 데코레이터를 붙이고, 해당 클래스를 불러 올 때는 Container에서 가져오는 방식이네요

 

설명은 주석으로 처리했습니다. 그렇다면 이 녀석을 우리 코드에 적용해봅시다.

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

@Service()
class Weapon {
  public attack() {
    console.log('hit!');
  }
}

@Service()
class Player {
  private weapon: Weapon;

  constructor(weapon: Weapon) {
    this.weapon = weapon;
  }

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

const me = Container.get(Player);
me.attack();

 

이와 같이 작성하면, Player 인스턴스를 생성하고, Player에 요구되는 종속성을 주입하기 위해 또 다시 인스턴스를 생성하는 일을 하지 않을 수 있게 됩니다.

 

의존성 주입과 IoC 컨테이너를 살펴보았으니 이후에 typedi를 좀 더 자세히 살펴보도록하도록합시다!


darren, dev blog
블로그 이미지 DarrenKwonDev 님의 블로그
VISITOR 오늘 / 전체