7장 의존성 주입 (1/2) - Node.js/Javascript 환경에서의 한 패키지 내의 의존성 관리

이 글은 Node.js/Javascript 환경에서의 한 패키지(App) 내의 모듈 간의 의존성을 관리하는 방법에 대해 다룬다. 명시하지 않은 경우 Javascript 환경임을 미리 밝힌다.

Typescript는 지금까지 많이 활용돼왔고 생태계가 성숙한 상태이므로, OOP 방식으로 문제 해결을 하려는 경우 Typescript가 적정 기술이라고 생각한다.


흔한 하드코딩 의존성(구현체 직접 import)의 예


1. 유독 언급이 적은 Node.js에서의 의존성 관리, 왜?

백엔드와 같이 쉽고 빠르게 규모가 커지고 기능 변경이 잦은 코드 베이스인 경우 설계가 중요한 경우가 많을 것이다. 설계는 의존성 관리가 기본이며 Node.js 백엔드 또한 그 예외는 아닐 것인데 말이다.

Q. 왜 Node.js에서는 하드 코딩된 의존 관계를 구축하는 코드를 찾기가 매우 쉬울까?

A. 가설: 인터페이스와 상관 없이 임의의 객체를 집어 넣어 테스트를 할 수 있기 때문에 굳이 Interface가 필요하지 않다. 동적 타입 언어이니까.


2. “동적 타입 언어”라는 특징

장점

만약 Java 였다면 Interface를 아예 사용하지 않는 것은 설계에 큰 문제가 있음을 시사하는 것이겠지만 Javascript는 동적 타입 언어이다. 기능을 실행하는 객체의 타입이 중요하지 않은 언어이다. 인터페이스가 없는 만큼 규칙도 없지만 그만큼 유연해진 셈이다.

단점

다만 동적 타입을 활용해 테스트가 가능하다고 해도 자연스럽게 생기는 강한 결합이 사라지는 것은 아니다. 구현체에 직접 의존하면 강한 결합이 발생한다. 의존하는 객체의 구현 상세에 대한 아무런 격리 장치가 없으며 구현체에서 변경이 생겼을 때 해당 의존성을 사용하는 모든 객체에 그 여파가 전달되므로 다시 검증(테스트), 빌드해야만 한다.


3. OOP의 문제 해결 방식

OOP 에서는 인터페이스를 미리 정의하고 해당 인터페이스를 최대한 변경하지 않음(Open Close Principle)을 통해 문제를 해결한다. 인터페이스에 의존함을 통해 구현 상세와 사용 객체를 진정으로 격리시킬 수 있으며 이는 의존성 관리에 매우 큰 역할을 한다.

의존성에 의한 강한 결합을 막는 수단은 현재로썬 서비스 로케이터 패턴과 의존성 주입이 있다.

이제부터 이 글은 Javascript/Node.js 에서의 의존성 주입에 대해 다룬다.


4. 서비스 로케이터 패턴

서비스 로케이터 패턴이란 “의존성이 있는 각 객체가 서비스 로케이터 객체만을 직접 의존하고, 각 객체는 서비스 로케이터에 의존성을 명시해 구현체를 받아오는 것“을 말한다. (서비스 로케이터 패턴에 대해 더 자세히 알고 싶다면 이 글을 참고하라.)

아래 예는 AuthController가 AuthService에 의존하는 코드이다.

1
2
3
4
5
6
7
8
9
// AuthController.js - AuthService에 의존한다.
// AuthController는 ServiceLocator에만 '직접' 의존한다.
module.exports = (serviceLocator) => {
const authService = serviceLocator.get('authService'); // TS 등 정적 타입 언어에서는 타입으로 받아온다.
// Javascript는 딱히 타입이 없으므로 String으로 의존성(객체)을 식별한다.
// require()와 사용 방식이 매우 닮아있다. 차이가 있다면, require는 전체 경로를 명시한다는 점이다.
const authController = {};
//...
}

서비스 로케이터 패턴의 장점

의존성의 구현체에 의존하지 않게 해준다. 이는 의존성 주입과 동일한 장점이며 아주 좋은 장점이다.

서비스 로케이터 패턴의 단점

객체의 구현 코드를 보지 않으면 곧바로 의존 관게를 파악할 수 없다. 생성자 등으로 명시하지 않기 때문에 - 생성자의 파라미터로 명시한다면 필수값이라는 문서화의 역할을 수행하게 되는데 비해 - 모든 객체에 대해 문서화가 필요하다.


5. 의존성 주입

의존 관계를 가장 잘 다루는 방법은 아마도 DI일 것이다. Javascript 진영에선 Angular가 최초로 의존성 주입을 도입한 것으로 안다(Typescript도 없던 시절이었는데!).

의존성 주입이란 “모듈의 의존성을 외부 개체에 의해 입력으로 전달 받는 것“을 말한다. 의존성 주입의 개념 자체는 매우 간단하다. DI를 지원하기 위한 컨테이너와 지원 방식을 구현하는 게 어려울 뿐이다.

(ex) AuthController가 AuthService에 의존하는 경우의 예시를 확인하자.

Before DI: 구현체를 직접 가져오는 모듈

1
2
3
4
5
6
7
// 직접 가져온다.
const authService = require('./authService');

exports.login = (req, res, next) => {
authService.login(...);
//...
};

After DI: 의존성을 받아오는 모듈

1
2
3
4
5
6
7
8
9
10
// authService를 전달 받아서 사용한다. authService의 출처와 구현체에 대해 아는 것은 더 이상 이 객체의 책임이 아니다. 그냥 사용만 하면 된다.
module.exports = (authService) => {
const authController = {};

authController.login = (req, res, next) => {
authService.login(req.body.username, req.body.password, ...);
//...
}
return authController
};

Service Locator / DI Container의 간략한 구현도 포함하려고 했으나 2편에서 다루도록 하겠다.


6. Node.js의 DI 컨테이너 생태계

약간의 짬을 내어 찾아보니 크게 4개의 오픈소스 컨테이너들이 있었다: InversifyJs, tsyringe, typedi, awilix (점유율 순).

tsyringe는 Microsoft에서 만들었다. 재밌는 점은 MS에서 inversifyjs를 사용한다고 나와있는 것이다. NestJs는 DI를 Core에 내장하여 차트에 포함시켰다.

dif

각 라이브러리의 자세한 비교는 기회가 된다면 추후 진행하려 한다.


TODO:

  1. Clean Architecture를 다시 읽는다. SOLID 원칙 조차 희미해진 듯하다.
  2. DI와 DIP의 관계에 대해 다시 공부해야겠다.
  3. 양파 껍질 Architecture에 대해 제대로 이해해야겠다.
  4. require과 서비스 로케이터 패턴의 관계에 대해 이해해야겠다.

CS에서 가장 자신있던 객체지향을 이렇게 모르게 됐다는 게 새삼 충격적이다 :(

7장 의존성 주입 (1/2) - Node.js/Javascript 환경에서의 한 패키지 내의 의존성 관리

https://jsqna.com/ndp-7-dependency-injection-1/

Author

Seongbin Kim

Posted on

21-02-23

Updated on

21-03-03

Licensed under

댓글