[JS]로 알아보는 DI vs IOC vs DIP

turtle601·2022년 11월 15일
17
post-thumbnail

학습목표
객체 설계 시 객체의 종속성을 낮추고 DI, IOC, DIP를 학습하면서 객체 시 좀 더 느슨한 결합을 통해 유지보수하기 좋은 코드를 만들 수 있다.

📌 IOC(제어의 역전)이란?

IOC(Inversion of Control)
소프트웨어 공학에서 제어 역전 ( IoC ) 은 프로그래밍 원리입니다. IoC는 기존 제어 흐름과 비교하여 제어 흐름을 반전시킵니다. IoC에서 컴퓨터 프로그램 의 사용자 지정 부분은 일반 프레임워크 에서 제어 흐름을 수신합니다 . 이 디자인 의 소프트웨어 아키텍처 는 기존의 절차적 프로그래밍 과 비교하여 제어를 역전 시킵니다.
...
위키피디아

위 글은 정말 이해가 잘 되지 않으니 예시를 들어서 한 번 설명해보겠습니다.

예시

어머니가 나에게 밥을 차려주는 상황을 예시로 한번 살펴보자.

1) 프로그램의 기존 제어 흐름(객체 간 종속성 up)

class Mom {
  constructor() {
    this.garlic = new Garlic();
    this.mugwort = new Mugwort(); 
  }
  
  cook () {
	// receipe
  }
}

const mom = new Mom();

mom.cook(); // 실행

이 경우 App -> Mom 객체 순으로 프로그램이 실행, 프로그램이 실행되면 우리 가족은 엄마가 가지고 있는 요리 중 쑥과 마늘만 먹게 되는 현상이 발생하게 된다.
(사람이 되겠습니다 ㅜㅜ)

2) 제어의 역전(객체 간 종속성 down)

class Mom {
   constructor(dish) {
     this.dish = dish;
   }
  
  cook () {
    console.log(`${this.dish.get()}요리 완성`)!!
  }
}

const mom = new Mom(new Hamburger());

이 경우 App -> Hamburger -> Mom 객체 순으로 프로그램이 실행, 나는 외부에서 햄버거를 가져와서 저녁시간 때 햄버거를 먹을 수 있다.

즉, 메서드나 객체의 호출 작업을 개발자가 직접 결정하는 것이 아니라, 외부에서 결정되는 것을 의미한다.

📌 DI(Dependency Injection)이란?

DI(의존성 주입)을 먼저 알아보기 전에 객체의 의존성에 대해 먼저 알아보자.

객체 의존성(Dependency)

쉽게 말해서 객체 A와 객체 B와의 연결을 뜻한다. 아래 코드를 한 번 살펴보자.

class Mom {
  constructor() {
    this.garlic = new Garlic();
    this.mugwort = new Mugwort(); 
  }
  
  ...
}

Mom 객체 인스턴스가 생성되면 Garlic 객체도 생성이 된다. 이처럼 객체 Mom과 Garlic이 연결되어 있는 상태를 객체 의존성이라고 말한다.

그렇다면 의존성 주입(DI)는 무엇일까?

위의 예제처럼 Mom객체 인스턴스가 생성되고 Garlic 객체가 생성이 되는 이런 객체 간 직접적인 연결이 아닌
서로 동등한 위치에서의 서로 다른 객체들끼리 의존성 관계를 지정하는 방법이다.

즉, 외부에서 가져온 객체와 또 다른 객체를 서로 연결하는 작업을 DI(Dependency Injection)이라고 한다.

쉽게 말해서 IOC 원칙을 지키는 일종의 패턴DI라고 한다.

DI를 구현하는 방법은 크게 두가지가 있다.

1. 생성자를 통한 주입
2. 외부에서 생성된 객체에 setter를 통한 의존성 주입

예시 1. 생성자를 통한 주입

// 위의 예시와 동일
class Mom {
   constructor(dish) {
     this.dish = dish;
   }
  
  cook () {
    console.log(`${this.dish.cook()}요리 완성`)!!
  }
}

const mom = new Mom(new Hamburger());

2. setter를 통한 주입

class Mom {
  setDish(dishInstance) {
    this.dish = dishInstance;
  }
  
  cook () {
    console.log(`${this.dish.cook()}요리 완성`)!!
  }
}
  
const mom = new Mom();
const hamburger = new Hamburger();

mom.setDish(hamburger);
mom.cook();

DI의 장점

앞서 예시를 DI를 어떻게 구현하는 지 살펴보았다.

위의 코드에서도 알 수 있듯이 각각의 객체가 종속되어 있는 것이 아니라 객체 각각의 기능에 집중할 수 있다는 장점이 있다.

각 객체 별로 기능이 집중되어 있다보니 테스트에도 용이하며 유연성과 확장성 또한 높아진다.

DI의 단점

class Car() {
  constructor() {
    // ...
  }
  
  ...
}

class Mom {
   constructor(dish) {
     this.dish = dish;
   }
  
  cook () {
    console.log(`${this.dish.cook()}요리 완성`)!!
  }
}

const mom = new Mom(new Car());

위의 예시는 Mom과 Car 객체과 생성자 주입을 통해 DI를 구현하였다.

우리 현실 세계에서는 차로 요리를 할 수도 없고, 위의 코드 내에서도 cook이라는 함수도 존재하지 않는다.

하지만 Mom객체는 아무것도 모르고 Car객체에게 요리를 하라고 명령을 하고 있다.

이처럼 Mom객체와 Car객체는 아무 이유없이 각자의 역할에 대해 서로 너무 믿고 있었고 의존적이었다고 볼 수 있다.

📌 DIP(Dependency Inversion Principle)란?

DIP(의존 역전 원칙)
상위 레벨의 모듈은 절대 하위 레벨 모듈에 의존하지 않는다. 둘다 추상화에 의존해야한다. (의존성을 분리)

위의 코드를 다시 한 번 예로 들어보자.

위의 코드에서 Mom 객체와 Car객체는 서로 너무 의존적이었다.
따라서 이를 해결하기 위해 요리 재료나 사람이 먹을 수 있는 요리를 추상적으로 정하고 제한을 두면 어떨까?

class Eatable {
  #ERROR_MESSAGE = 'OVERIDING_ERROR'
  
  cook() {
    throw new Error(#ERROR_MESSAGE);
  }
}

class Hamburger extends Eatable {
  cook() {
    // 대충 햄버거 요리한다는 내용
  }
}

class Mom {
   constructor(dish) { 
     this.dish = dish;
   }
  
  cook () {
    console.log(`${this.dish.cook()}요리 완성`)!!
  }
}
  
const mom = new Mom(new Hamburger);
mom.cook();

즉, 위의 그림처럼 Mom 객체는 Interface 객체인 Eatable에 의존, 하위 클래스인 Hamburger 클래스는 Interface 객체인 Eatable에 의존한다. (JS에서는 타입이 존재하지 않아 이런식으로 밖에 구현을 할 수가 없다ㅜㅜ)

Mom - Hamburger 객체가 서로 직접적인 연결(의존)되어있는 것이 아니라 서로 의존성을 분리되어있다.

이를 통해서 앞서 DI에서 오류가 발생하는 객체인 Car 객체에 대한 의존성 주입을 미리 차단할 수 있고 DIP 원칙을 통해 의존성을 분리하면서 좀 더 유지보수하기 좋은 객체 구조를 만들 수 있다.

DIP의 자세한 내용은 객체지향의 5원칙 SOLID 에서 확인 가능하다. (제가 직접 쓴 글입니다.)

참고 자료
테크톡 DI, IOC
5분만에 알아보는 DI

profile
Programmer

2개의 댓글

comment-user-thumbnail
2022년 11월 16일

좋은 글 감사합니다!

1개의 답글