IoC와 DI란 무엇인가요?

calm0_0·2023년 7월 5일
0

디자인패턴

목록 보기
1/2
post-thumbnail

Intro


NestJS를 사용하다 보면 IoC와 DI라는 용어가 종종 언급된다. 이러한 용어를 몰라도 기능 구현에는 문제가 없겠지만 NestJS에 대해 좀 더 깊이 있게 이해하려면 공부해봐야겠다는 생각이 들었다. 그래서 이번 시간에는 IoC와 DI에 대해 공부하면서 이해한 내용을 정리해보려고 한다.


IoC를 적용하기 이전


class Warrior {
  ...
  const sword = new Sword();
  ...
}
  
class Sword {
  ...
}

다음과 같이 Warrior 클래스에서 Sword 클래스의 인스턴스를 생성했다. 여기서 Warrior 클래스는 Sword 클래스에 의존하고 있다. Warrior 클래스는 Sword 클래스를 직접 참조하고 있으며, 이 클래스 없이 완성할 수 없기 때문이다.

의존성(Dependency)이란 하나의 객체에서 다른 객체를 가져다 사용하는 것을 의미한다. 'A가 B에 의존한다'라는 것은 B가 변하면 A에 영향을 미치는 관계를 말한다. (A -> B)

만약, 여기에 Warrior가 사용할 수 있는 무기로 도끼나 몽둥이를 추가해달라고 하면 어떻게 될까? 새롭게 도끼와 몽둥이 클래스를 만들고, Warrior 클래스에서 Sword 인스턴스를 생성하는 부분을 수정해야 한다. 무기 종류가 더 많아진다면 Warrior 클래스의 코드가 늘어나고, 코드의 중복도 많아질 것이다.

어떤 객체가 다른 객체에 의존하고 있을 때, 의존하는 객체에 변경이 생기거나 다른 객체를 사용해야 하는 경우 관련 코드를 전부 바꿔야 한다. 이처럼 기존 로직에 새로운 값이 추가되거나 변경될 때 코드 수정이 많은 코드를 확장성이 적은 코드라고 하며, 객체간 의존성이 강하기 때문에 결합도가 높은 코드라고 한다. 바람직하지 않은 코드인 것이다.

이러한 의존 관계로 인한 문제를 해결하기 위해 인터페이스를 사용했다. A가 B에 의존하고 있다면, B에 대한 인터페이스를 정의하고, A에서는 IB 타입을 이용한다. 위의 예시에서 인터페이스를 활용하여 전사와 무기 모두 확장 가능하도록 변경해보자.

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!');
  }
}

몽둥이를 가진 전사 클래스를 인스턴스화하면 다음과 같다

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

이제 Warrior 클래스(A)에서는 무기의 종류가 바뀌어도 하나 하나 수정할 필요가 없어졌다.
하지만, interface B의 구현체(몽둥이)를 직접 생성해야건 여전히 문제로 남아있다.

제어의 역전을 활용하면 이러한 문제를 해결할 수 있다. (아래 예제에서는 typedi 를 사용)

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

...

@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를 이용한 인스턴스 생성이 없지만 잘 동작한다. 이는 typedi 의 컨테이너가 알아서 필요한 인스턴스를 생성했기 때문이다. 이처럼 제어권을 내가 아닌 외부로 넘기는 것이 제어의 역전이다.


제어의 역전(IoC, Inversion of Control)


위의 예제에서 보았던 제어의 역전에 대한 개념적인 내용을 살펴보자.

제어의 역전이란?

일반적으로 프로그램의 흐름은 모든 오브젝트가 능동적으로 자신이 사용할 클래스의 인스턴스를 생성하고, 사용하고, 폐기하는 과정을 담당한다. 즉, 자신이 사용할 오브젝트의 생명주기를 자기 자신이 담당하는 것이다.

제어의 역전은 이러한 일반적인 관점을 뒤집어, 더 이상 자신이 사용할 오브젝트의 생명주기를 자기 자신이 담당하지 않는 것을 의미한다. 즉, 자기 자신이 가지고 있던 오브젝트의 생명 주기에 대한 제어 권한을 이런 것들을 대신 관리해주는 컨테이너로 넘겨주는 것이다.

IoC의 이점

  • 객체를 관리해주는 독립적인 존재(컨테이너)와 그 외 내가 구현하고자 하는 부분으로 각각 관심사를 분리하고 서로의 역할에 충실하면서 변경에 유연한 코드를 작성할 수 있다.
  • 개발자는 객체의 생명 주기 관리를 신경쓰지 않고 메인 로직에 집중할 수 있다.
  • 엔터프라이즈 차원에서 수많은 객체들을 편리하게 관리할 수 있다.
  • 유연성과 확장성이 증가한다.

의존성 주입(DI, Dependency Injection)


의존성 주입이란

의존 관계에 있는 객체들이 있을 때, 필요한 객체를 직접 생성하는 것이 아니라 외부(컨테이너)에서 객체를 전달해 주입해줌으로써 객체 간 결합도를 줄이고 유연한 코드를 만드는 방식을 의존성 주입이라고 한다.

추상적인 IoC를 구현한 것이 DI이다.(DI를 통해 IoC를 구현)

NestJS는 DI를 통해 IoC를 구현한 프레임워크이다.

의존성 주입의 장점

  • 모듈들을 쉽게 교체할 수 있는 구조가 된다.
  • 단위 테스팅과 마이그레이션이 쉬워진다.
  • 애플리케이션 의존성 방향이 좀 더 일관되어 코드를 추론하기 쉬워진다.

의존성 주입의 단점

  • 종속성 주입 자체가 컴파일을 할 때가 아닌 런타임 때 발생하므로 컴파일을 할 때 종속성 주입에 관한 에러를 잡기가 어려워질 수 있다.


Reference
https://www.wisewiredbooks.com/nestjs/overview/04-provider.html
https://tristy.tistory.com/40
https://m.blog.naver.com/fbfbf1/222620699725
https://donggeun.co.kr/view/640a024cc9ee667c939ec402
https://wbluke.tistory.com/9
https://biggwang.github.io/2019/08/31/Spring/IoC,%20DI%EB%9E%80%20%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C/

profile
공부한 내용들을 정리하는 블로그

0개의 댓글