1. Dependency


종속성이란 무엇일까?

e.g.

class Engine {
  hp(input:number) {
    return `This vehicle has ${input} horsepower`;
  }
}

class Truck {
  truckInfo:Car
  constructor() {
    this.truckInfo = new Engine();
  }

  getHp(hp:number) {
    return this.truckInfo.hp(hp)
  }
}

const truck = new Truck();
console.log(truck.getHp(40)); // "This vehicle has 40 horsepower"

간단한 예시 코드를 작성해 봤다. Truck 클래스 안에 있는 truckInfo라는 프로퍼티는 생성자 함수 안에서 Engine 클래스의 인스턴스를 할당 받는다. Truck은 스스로 Engine에 대한 종속성을 생성하고 있다. 이런 구조는 Truck이 Engine에 의존하는 형태이다. 이런 것을 종속성이라고 한다. 의존하는 관계, 그것이 종속성이다.

이처럼 종속성을 가지고 있으면 몇 가지 단점이 발생한다. Engine 코드가 수정이 됐을 때 Truck의 코드도 수정해야 할 가능성이 높고, 테스트 코드를 작성한다고 했을 때도 종속성을 가지고 있기 때문에 unit test의 독립성을 떨어뜨린다. 테스트 속도 면에서, 그리고 편의성 면에서도 단점이 있는데 이는 실제 서비스 코드를 떠올려 보면 보통 service 클래스에서 repository 클래스를 이용해서 DB에 CRUD를 한다. 위 예시 코드 처럼 service 클래스가 repository 클래스에 종속되어 있다면, 테스트 환경에서도 실제 DB에 CRUD를 할 것이고 메모리를 사용하는 것 보다는 상대적으로 속도가 느리다. 하지만 가짜 데이터와 가짜 repository 클래스를 만들어서 모킹을 하고 메모리에서 CRUD를 하면 속도도 상대적으로 더 빠르고, 실제 DB를 사용하는 것이 아니기 때문에 훨씬 더 편하기도 하다. 이렇게 하기 위해서는 IoC를 적용해야 한다.

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


Inversion of Control(IoC)은 우리말로 '제어 역전'으로 말이 좀 생소하거나 어려울 수 있지만 말 뜻을 그대로 받아들이면 된다. Truck 클래스 입장에서는 Engine 클래스가 필요하지만 생성자 함수에서 직접 초기화 하지 않으면서 스스로 종속성을 생성하지 않는 것이다. 즉, 누군가가 외부에서 이를 제어하는 것이다. 위 예시 코드를 조금 수정해보자

class Engine {
  hp(input:number) {
    return `This vehicle has ${input} horsepower`;
  }
}

class Truck {
  constructor(public truckInfo:Engine) {}

  getHp(hp:number) {
    return this.truckInfo.hp(hp)
  }
}

const engine = new Engine();
const truck = new Truck(engine);
console.log(truck.getHp(40)); // "This vehicle has 40 horsepower"

이렇게 생성자 함수에서는 Engine 클래스로 타입만 해주면 된다. 이런 방식으로 사용하면 사용하는 부분에서 Truck 클래스를 제어하게 되고 이 부분에서 제어의 역전이 발생한 것이다. 또 Truck에는 Engine으로 된 타입만 들어오면 코드가 돌아가니까 테스트를 위해 가짜 클래스를 연결하는 것, 즉 모킹하는 것도 편리해진다. 이를 통해서 앞에서 말했던 문제들을 해결할 수 있다. 하지만 이것도 나름의 단점이 있다.

마지막에 Engine 인스턴스를 생성하고 Truck 인스턴스를 생성하는 부분이 있는데, 만약 규모가 큰 어플리케이션에서 종속성이 복잡할 때는 저 부분이 여러 줄로 많아질 것이다. 그런 부분에서 Nest는 DI Container를 사용하면 된다.