의존성 주입 in TypeScript (1) - 의존성 분리 원칙과 의존성 주입

Server The SOPT·2022년 4월 8일
14

DI-in-TS

목록 보기
1/2
post-thumbnail

작성자: 이현우
작성자의 한마디: 개발을 하면서 정말 중요하다고 생각하는 지식들 중 하나이기에 여러분들께 공유를 하고자 합니다.

개요

프로그램을 "잘" 작성했다는 기준을 어떻게 세울 수 있을까요? 코드를 짧게 하는 것이 좋은 코드다, 다른 사람들이 읽었을 때 보기 좋은 코드가 좋은 코드이다 등 N명의 사람들에게 N개 혹은 그 이상의 기준들이 있을 수 있습니다.

하지만 어떤 질을 측정하기 위해서는 보편적으로 존재해야하는 기준이 존재해야 하기에, 현대 개발 생태계에서는 유지보수가 쉽고 기능 확장이 용이한 것을 그 기준으로 잡고 있습니다. 그렇다면 유지보수가 쉽고 기능 확장이 용이하게 프로그램을 설계하고 제작하기 위해서는 어떻게 해야할까요?

객체지향 설계 원칙: SOLID

2000년대 초 컴퓨터 과학자 Robert Martin은 이와 관련된 다섯 가지 원칙을 소개했습니다. 이 원칙들은 그 앞 글자들을 따 SOLID라는 이름으로 불리게 되었는데요, 이는 다음과 같습니다.

  • Single Responsibility Principle: 단일 책임 원칙
  • Open Closed Principle: 개방 폐쇄 원칙
  • Liskov Subsitution Principle: 리스코프 치환 원칙
  • Interface Segregation Principle: 인터페이스 분리 원칙
  • Dependency Inversion Principle: 의존성 역전 원칙

그 중에서 이번 글은 가장 마지막 원칙인 의존성 역전 원칙의 의의와 이 원칙의 구현체를 다뤄볼 것입니다.

의존성 역전 원칙

  1. 상위 레벨의 모듈(하위 레벨의 모듈을 활용하여 판단을 내리는 모듈)은 하위 모듈에 의존해서는 안된다. 두 모듈 전부 추상화에 의존해야한다.
  2. 추상화는 구체적인 것에 의존해서는 안된다. 구체적인 것은 추상화에 의존해야한다.

말이 좀 어렵네요. 처음부터 차근차근 이해를 해봅시다.

음...그래..상위 모듈이 하위 모듈을 이용해서 판단을 내리는 모듈이라고...그런데 하위 모듈을 의존해서는 안된다고...?

이게 과연 무슨 소리일까요?

예시

자 여러분은 아주 고급진 램프를 하나 구매했습니다. 이 램프를 침대 옆에 두고 밤이 되면 램프를 끄고 잠에 들겠죠? 우리가 흔히 사용하는 램프의 사용패턴은 스위치를 활용하여 램프를 끄는 것입니다.

그런데 시간이 자나서 이제 리모콘으로도 스위치뿐만 아니라 리모콘이나 스마트폰으로도 램프를 켜고 끌 수 있는 램프가 출시되었다고 생각을 해봅시다. 위와 같이 버튼에만 의존하여, 즉 버튼을 활용하는 램프는 새로 나온 램프의 기능(리모콘으로 램프를 켜고 끌 수 있는 기능)을 추가할 수 가 없을 것입니다. 만약 추가한다고 하더라고 Lamp의 일부분을 뜯어 고쳐서 개조를 해야겠죠.

하지만 만약에 램프를 만들때부터 "키고 끄는 기능"을 따로 모듈화 했었으면 어떨까요? 그 키고 끄는 기능을 현재는 버튼을 통해 구현을 했지만, 이후 더 좋은 기능이 나온다면 이는 리모콘, 스마트폰으로도 램프를 키고 끄는 기능을 동작시킬 수 있을 것입니다.

소결

자 그렇다면 위의 설명이 이해가 가실까요?
현재 상위 모듈(램프를 키고 끌 수 있는 버튼)은 하위 모듈(램프)를 직접 참조 하지 않고 키고 끌 수 있는 기능(Switchable)을 참조하여 기능을 구현하고 있습니다. 마찬가지로 Lamp도 Switchable을 참조하고 있구요.

이와 같이 상위 모듈/하위 모듈 간 참조 관계를 인터페이스로 분리한다면 변동성이 큰 실제 구현체를 참조하지 않기에 상위 모듈에서 큰 코드 변경 없이 기능 변경을 할 수 있게 됩니다.

Dependency Injection(DI): 또 한 번의 관심사 분리

그렇다면 위의 예제를 TypeScript로 작성해볼까요?

interface Switchable {
  turnOn(): void;
  turnOff(): void;
}

class Button implements Switchable {
  turnOn() {
    // TODO
  }  
  turnOff() {
    // TODO
  }  
}

class SmartPhone implements Switchable {
  turnOn() {
    // TODO
  }  
  turnOff() {
    // TODO
  }  
}

class Lamp {
  switch: Switchable = Button();
}

const lamp = Lamp();

그렇다면 만약에 switch를 SmartPhone으로 바꾸고 싶다면 어떻게 할까요? Button()을 다시 SmartPhone()으로만 바꾸면 될까요? 만약에 SmartPhone말고도 다른 스위치 기능 구현체들이 많다면 어떻게 해야할까요?

이 스위치의 교체를 용이하게 하기 위하여 스위치를 멤버 변수에다가 고정하지 않고 외부에서 주입받게 할 수도 있습니다.

class Lamp {
  _switch: Switchable;
  constructor(switch: Switchable) {
    this._switch = switch;
  }
}

위와 같은 방식으로 외부에서 생성받은 객체(의존성)를 받아오는 것을 의존성 주입(Dependency Injection)이라고 합니다. 이와 같이 구현을 하게 되는 경우 객체를 생성할 때 어떤 의존성을 활용할 것인지 "직접" 지정하지 않고도 특정 서비스를 사용할 수 있게 됩니다.

더 쉽게 풀어말하자면, 기존 코드에서는 램프가 버튼을 사용할 지, 스마트폰을 사용할 지 램프단에서 직접 지정하여 사용을 해야 했다면, 변경된 코드에서는 외부에서 이 램프를 만들 때 버튼을 사용하는 램프, 스마트폰을 사용하는 램프를 지정하여 만들 수 있습니다.

즉 객체를 사용하면서 객체를 생성하는 과정 그 자체의 관심사를 또 분리하여 개발자로 하여금 코드의 응집성을 더욱 높이게 할 수 있는 패턴인 것이라고 이해를 할 수 있습니다.

이후 글에서 의존성 주입을 더욱 잘 활용하게 할 수 있는 IoC(Inversion of Control) 개념과 TypeScipt에서 사용할 수 있는 IoC Contianer Framework인 Ineversify.js를 소개해보도록 하겠습니다.

참조

profile
대학생연합 IT벤처창업 동아리 SOPT 30기 SERVER 파트 기술 블로그입니다.

2개의 댓글

comment-user-thumbnail
2022년 4월 10일

inversify 요즘 쓰는중인데 다음 포스팅 넘 기대됩니닷

1개의 답글