의존성 주입과 역전 원칙, 제어의 역전

1. 개요

두 달 전에 Nest.js를 얕게 공부해 본 경험이 있으며, 특히 처음 보는 디자인 패턴에 신기했었습니다. (Spring을 하시는 분들이 보시면 이거 Spring 복제품 아님?이라고 할 정도로 비슷한 면이 많은 것 같습니다). 그래서 Express로 Nest.js와 같은 형태를 나타낼 수 있을까?라는 의문을 품고, 프로젝트를 진행하였습니다(공식 문서를 보면서 완전히 똑같히 구현하려고 하면 단순히 따라하는 것 밖에는 되지 않는다고 생각하여 기억을 더듬어가며 스스로 구현해보았습니다).

기능 구현은 무사히 마쳤으나, 전부 클래스로 구현하다보니 테스트 코드를 작성하는 중에 의존성 이슈가 발생하였습니다(현재 제가 작성한 코드의 구조를 간략하게 나타내자면 다음과 같습니다).

- App
  - Controller
    - Pipe
    - Service
      - Repository
        - Model
          - Sequelize

예전부터 궁금하기도 했고, 이참에 간단하게 개념이라도 알고 넘어가야겠다 싶어서 아래 내용에 대해서 정리해보았습니다.

  • 의존성으로 인한 문제를 해결할 수 있는 방법은 무엇인가?
  • 의존성 주입은 무엇이며 왜 해야 하는가?
  • 의존성 주입의 단점은 어떻게 극복해야 하는가?

2. 의존성

의존성은 말 그대로 어떠한 것에 의존하는 특성을 말하는데, 의미 자체가 추상적이기 때문에 한 가지 예를 들어보겠습니다. 먼저, 다음과 같이 무기(Weapon)와 캐릭터(Character) 클래스가 있다고 가정해봅시다.

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

class Character {
  constructor(private weapon: Weapon = new Weapon()) {}

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

const character = new Character();
character.attack();

위 코드를 보면 Character 클래스는 Weapon 클래스를 내부로 호출하여 무기를 소지하고 있는 캐릭터가 되었습니다. 즉, Character 클래스는 Weapon 클래스에 의존적이라고 할 수 있습니다. 그런데, ‘무기’라는 의미는 추상적이기 때문에 무기의 종류 중 하나인 도끼(Axe)로 변경(구체화)하려고 합니다. 이를 코드로 나타내면 다음과 같습니다.

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

class Axe extends Weapon {
  public attack() {
    console.log('chop!');
  }
}

class Character {
  constructor(private weapon: Weapon = new Axe()) {}

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

const character = new Character();
character.attack();

도끼는 무기의 한 종류이기 때문에 Weapon 클래스를 상속 받았고, 찍기(chop)라는 공격 방식으로 오버라이딩 하였습니다. 그런데, 무기의 종류에는 도끼 뿐만 아니라 활, 표창 등 다양하게 존재할 수 있습니다. 그러므로, 캐릭터의 무기를 변경할 때마다 Weapon에 의존적인 또 다른 ‘무기 종류’ 클래스로 바꿔주는 작업이 필요합니다. 뿐만 아니라, 새로운 무기의 메소드, 해당 클래스를 참조하는 외부의 코드까지 모두 수정해주어야 합니다. 이것이 바로 의존성의 단점입니다.

3. 의존성 주입(DI)

위에서 살펴본 의존성의 단점을 극복하는 좋은 방법 중 하나가 바로 의존성을 주입(Dependency Injection)하는 것 입니다.

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

의존성 주입을 하는 방법은 매우 간단합니다. 의존성을 그대로 사용하는 경우에는 생성자 없이 클래스 내부로 직접 새로운 인스턴스를 호출했다면, 의존성 주입은 생성자를 통해 의존성을 주입하는 방식입니다.

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

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

만약, 캐릭터의 무기의 종류를 바꿔줄 때, 의존성을 그대로 사용하는 경우에는 직접 캐릭터 클래스 뿐만 아니라 기존의 무기, 새로운 무기 클래스와 연관된 모든 코드를 수정해주어야 했지만, 의존성을 주입하게 되면 다음과 같이 간편하게 새로운 무기로 바꿔줄 수 있습니다.

const character = new Character(new Axe());

4. 의존성 역전 원칙(DIP)

의존성을 그대로 사용하는 경우, 캐릭터의 의존성(무기)을 무기의 종류로 직접 결정하였습니다.

  • 무기 - 도끼:무기 - 캐릭터(무기 = 도끼

반면, 의존성을 주입하는 경우에는 캐릭터에서 의존성을 결정하는 모습을 볼 수 있습니다.

  • 캐릭터(무기 = 도끼) - 도끼:무기 - 무기

이와 같은 모습을 의존성 역전이라고 하며, 이러한 원리를 의존 관계 역전의 원칙(Dependency Inversion Principle, DIP)이라고 합니다. 그런데, 이 경우에도 한 가지 단점이 존재합니다. 예를 들어, 캐릭터의 종속성은 무기 뿐만 아니라 갑옷, 투구, 신발 등 다앙하고 많이 존재할 수 있습니다. 이를 위해서 아래와 같이 새로운 인스턴스를 매번 생성할 것이고, 이러한 단순 작업은 곧 인적 오류를 불러일으킬 수 있습니다.

const character = new Character(new Axe(), new Armor(), new Shield());

5. 제어의 역전(IoC)

의존성 역전의 원칙의 단점으로는 종속성 증가에 따라 단순 반복 증가 및 그로 인한 인적 오류 발생 확률 증가라고 정리할 수 있습니다. 만약, 외부에서 종속성을 한 군데 모아놓은 객체를 생성하고, 이를 캐릭터에게 주입하면 관리하기에 훨씬 편할 것 입니다. 즉, 종속성에 해당하는 클래스의 인스턴스를 개발자가 직접 관리하지 않고, 별도의 타입으로 관리(TypeDI)하는 상태이며, 별도의 타입 객체를 IoC 컨테이너라고 합니다. 그리고 이러한 개념을 제어의 역전(Inversion of Control, IoC)이라고 합니다.

6. 참고자료

  • https://brownbears.tistory.com/581
  • https://peter-cho.gitbook.io/book/11/clean-architecture/5
  • https://velog.io/@tataki26/%EC%9D%98%EC%A1%B4-%EA%B4%80%EA%B3%84-%EC%97%AD%EC%A0%84-%EC%9B%90%EC%B9%99
  • https://darrengwon.tistory.com/1363

"" 개의 결과를 찾았습니다.

    ""에 대한 결과를 찾을 수 없습니다.