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