Search

의존성 주입(DI)과 IoC (with node.js)

주제
Backend
날짜
2023/01/13

개요

프로그램을 "잘" 작성했다는 기준을 어떻게 세울 수 있을까?
코드를 짧게 하는 것이 좋은 코드다, 다른 사람들이 읽었을 때 보기 좋은 코드가 좋은 코드이다 등 N명의 사람들에게 N개 혹은 그 이상의 기준들이 있을 수 있다.
하지만 어떤 질을 측정하기 위해서는 보편적으로 존재해야하는 기준이 존재해야 하기에, 현대 개발 생태계에서는 유지보수가 쉽고 기능 확장이 용이한 것을 그 기준으로 잡고 있다.
그렇다면 유지보수가 쉽고 기능 확장이 용이하게 프로그램을 설계하고 제작하기 위해서는 어떻게 해야할까?

객체지향 설계 원칙: SOLID

SOLID
Single Responsibility Principle : 단일 책임 원칙
Open Closed Principle : 개방 폐쇄 원칙
Liskov Subsitution Principle : 리스코프 치환 원칙
Interface Segregation Principle : 인터페이스 분리 원칙
Dependency Inversion Principle : 의존성 역전 원칙
그 중에서 이번 글은 가장 마지막 원칙인 의존성 역전 원칙의 의의와 이 원칙의 구현체를 다룰 것이다.

의존성 역전 원칙

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

예시

램프의 사용패턴은 스위치를 활용하여 램프를 끄는 것이다.
그런데 시간이 자나서 이제 리모콘으로도 스위치뿐만 아니라 리모콘이나 스마트폰으로도 램프를 켜고 끌 수 있는 램프가 출시되었다.
위와 같이 버튼에만 의존하여, 즉 버튼만을 활용하는 램프는 새로 나온 램프의 기능(리모콘으로 램프를 켜고 끌 수 있는 기능)을 추가할 수 가 없을 것이다.
만약 추가한다고 하더라고 램프의 일부분을 뜯어 고쳐서 개조를 해야한다.
하지만 만약에 램프를 만들때부터 "키고 끄는 기능"을 따로 모듈화 했었으면 어떨까?
그 키고 끄는 기능을 현재는 버튼을 통해 구현을 했지만, 이후 더 좋은 기능이 나온다면 이는 리모콘, 스마트폰으로도 램프를 키고 끄는 기능을 동작시킬 수 있을 것이다.

정리

현재 상위 모듈(램프를 키고 끌 수 있는 버튼)은 하위 모듈(램프)를 직접 참조 하지 않고 키고 끌 수 있는 기능(Switchable)을 참조하여 기능을 구현하고 있다. 마찬가지로 Lamp도 Switchable을 참조하고 있다.
이와 같이 상위 모듈/하위 모듈 간 참조 관계를 인터페이스로 분리한다면 변동성이 큰 실제 구현체를 참조하지 않기에 상위 모듈에서 큰 코드 변경 없이 기능 변경을 할 수 있게 된다.

Dependency Injection (DI)

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();
JavaScript
복사
그렇다면 만약에 switch를 SmartPhone으로 바꾸고 싶다면 어떻게 할까? Button()을 다시 SmartPhone()으로만 바꾸면 될까? 만약에 SmartPhone말고도 다른 스위치 기능 구현체들이 많다면 어떻게 해야할까?
이 스위치의 교체를 용이하게 하기 위하여 스위치를 멤버 변수에다가 고정하지 않고 외부에서 주입받게 할 수도 있다.
class Lamp { switch: Switchable; constructor(switch: Switchable) { this.switch = switch; } }
JavaScript
복사
위와 같은 방식으로 외부에서 생성받은 객체(의존성)를 받아오는 것을 의존성 주입(Dependency Injection)이라고 한다.
이와 같이 구현을 하게 되는 경우 객체를 생성할 때 어떤 의존성을 활용할 것인지 "직접" 지정하지 않고도 특정 서비스를 사용할 수 있게 된다.
즉, 객체를 사용하면서 객체를 생성하는 과정 그 자체의 관심사를 또 분리하여 개발자로 하여금 코드의 응집성을 더욱 높이게 할 수 있는 패턴인 것이라고 이해를 할 수 있다.

Inversion of Control (IoC)

Inversion of Control(IoC) is a programming principle. In traditional programming, the custom code that expresses the purpose of the program calls into reusable libraries to take care of generic tasks. But with inversion of control, it is the framework that calls into the custom, or task-specific, code. Educative.io
IoC(Inversion of Control)은 프로그램의 제어 흐름, 즉 호출 구조를 뒤바꾸는 것이다.
기존 문제점
처음 콘솔 프로그램을 작성하다보면 main함수에서 프로그램을 실행하고, main 함수에서 돌아갈 클래스를 정의, 객체를 생성하여 프로그램을 제작한다.
하지만 이런 구조로 프로그램을 작성할 경우 특정 부분은 기존 팀원과 본인들만 이해할 수 있는 코드로 작성을 할 수 있게 될 것이고 프로그램의 규모가 커져서 다른 사람들이 이 프로그램에 손을 대려고 할 때 처음부터 모든 프로그램의 흐름을 스스로 파악해야하기 때문에 개발 생산성이 크게 떨어질 것이다.
또한 어떤 로직은 그 컨텍스트에만 강결합되게 작성되어 유지보수가 힘들게 코드를 작성할 수 있다는 위험성도 높아질 수 있게 될 것이다.
IoC를 이용해 해결
IoC는 이런 상황을 줄여주기 위해 나온 개념이라고 보면 된다.
프로그램의 제어 흐름, 즉 프로그램의 메인 스트림을 미리 작성되었던 코드(프레임워크)에 의해 제어되게 하고 개발자가 작성한 코드(어플리케이션 코드)는 프레임워크에 의해 제어된다.
프레임워크 → 코드 → 프로그램
즉, 프로그래머가 짠 코드가 프레임워크에 의해 제어되는 제어권의 역전을 활용하여 프로그램 작성에 일관성을 부여하고, 유지보수가 가능하게 하는 코드를 작성하는데 도움을 줄 수 있게 하는 것이다.
이런 개념은 의존성 주입에서도 적용할 수 있다.
의존성 주입을 활용할 때, 객체 생성 시 의존성을 어플리케이션 코드(정확히는 객체 생성시의 코드)에서 특정하지 않아도 그 기능을 활용할 수 있다.
IoC는 객체를 생성하는 역할을 가지는 오브젝트를 정의하여 객체를 프로그래머가 직접 생성자를 호출하여 생성하는 것이 아니라 그저 의존관계만 설정하면 자동으로 객체를 주입하게 하여 어플리케이션 코드에서 객체 생성/생명주기 관리의 책임을 덜 수 있게 도와준다.

IoC의 구현체: IoC Container

대부분의 프레임워크에서는 IoC을 구현한 구현체들이 내장되어 있다.
대표적으로 Spring Framework의 Spirng IoC가 대표적이다.
IoC is also known as dependency injection (DI).
It is a process whereby objects define their dependencies (that is, the other objects they work with) only through constructor arguments, arguments to a factory method, or properties that are set on the object instance after it is constructed or returned from a factory method.
Spring IoC 프레임워크를 활용하면, 객체를 생성하고 생명주기를 관리하는 IoC 컨테이너에서 의존관계가 설정된 객체에 생성자/Setter를 통해 필요한 객체를 주입한다.
// Kotlin SpringBoot @RestController class SampleController( // Constructor Injection 가능 private val service: SampleService ) { // SampleService 활용 가능 @GetMapping("") fun getSomething() = service.getSomething() }
Kotlin
복사
하지만 현재 사용하고 있는 Express에서는 이런 IoC 컨테이너에 해당되는 기능이 내장되어 있지 않아 npm에서 IoC Container 라이브러리를 찾아야한다.

InversifyJS

InversifyJS is a lightweight (4KB) inversion of control (IoC) container for TypeScript and JavaScript apps.
A IoC container uses a class constructor to identify and inject its dependencies.
InversifyJS는 NodeJS 기반의 어플리케이션 위에서 동작하는 IoC Container이고 생성자 주입으로 객체간의 의존관계를 설정하는 방법을 제공한다.
InversifyJS에서는 런타임 오버헤드를 최대한 줄이면서 JS 개발 생태계에서 SOLID 원칙을 준수하여 어플리케이션을 개발할 수 있도록 도와주고 궁극적으로는 DX(Development Experience)를 높여주는 데 의의를 두고 있다.

요약

DI
DI(Dependency Injection)는 객체간의 의존성을 자신이 아닌 외부에서 주입하는 개념이다. 객체 자체가 아니라 Framework에 의해 객체의 의존성이 주입되는 설계 패턴이다.
Framwork에 의해 주입되므로 여러 객체 간의 결합이 줄어들고 재사용성이 증가한다.
IoC
IoC란 객체를 생성하고 객체 간의 의존 관계를 프레임워크가 대신 해주는 것을 의미한다.
개발자가 직접 객체를 생성하지 않고 의존 관계를 관리할 수 있게 된다.
객체를 제어하고 관리하는 역할이 개발자로 부터 프레임워크에게 역전 된다.