지난번에 적었던 의존성 하드코딩 부분부터 다시 보도록 하겠습니다.
// config.js
const config = require('./config.json');
module.exports = {
config: { ...config, host: 'xxx...' },
};
// test1.js
const { config } = require('./config');
const tmp = () => {
console.info(config.host);
};
tmp(); // "xxx..."
이런식의 코드 구성에선 test1.js 가 결국 config.json 에 종속되게 되죠. 이렇게 종속성을 하드코딩하면 재사용성이 낮아지며
단위 테스트가 어려워집니다. test1.js 를 단위 테스트하려면 결국 config.json 까지 준비가 되어있어야 하니 말이죠.
그래서 간혹 회사에서도 작업된 내용들을 보면 테스트 코드를 돌리는데 앱을 한번 띄웠다가 내리거나, config.json 을 다운받는 코드를
분리해서 추가하는 등의 기괴한(?) 구성을 하는 경우가 있습니다.
물론 하드코딩을 하면 직관적이라서 이해가 쉽긴 하지만, 가장 중요한 유지보수성을 생각하면 결합도는 낮춰야하며, 이러한 의존성을
어떻게 해결할건데? 하는 부분을 보고자 합니다.
- 의존성 주입
Dependency Injection 이라는 용어와 개념은 설명을 따로 하지 않아도 될 정도로 널리 퍼져있죠.
핵심은 그저 종속성들을 외부 개체에 의해 입력으로 제공하는 것입니다. 이런 외부 개체라는 것은 시스템의 모든 모듈 연결을 관리하는
컴퍼넌트 또는 전역 컨테이너 일 수 있습니다. DI 를 하기 위해 전용 컨테이너를 따로 만들기도 하니까요.
이걸 왜 쓰는데? 하면 당연히 위에서 말한 것처럼, 모듈에 대한 디커플링이 향상된다는 것입니다.
위 코드를 간단하게 리팩토링 해보겠습니다.
// config.js
const config = require('./config.json');
module.exports = {
config: { ...config, host: 'xxx...' },
};
// test1.js
const test1 = (config) => {
console.info(config.host);
};
module.exports = {
test1,
};
// app.js
const { config } = require('./config');
const { test1 } = require('./test1');
test1(config); // "xxx..."
다른 부분은 차치하고 test1.js 부분만 먼저 보시면 됩니다. 기존엔 config.js 의존성을 하드코딩했지만, 이젠 함수의 입력으로
받고 있습니다. 이제 test1.js 은 하드코딩된 의존성을 갖지 않으며 유일한 의존성은 입력으로 해결이 되었습니다.
test1.js 가 갖고 있던 config.json 에 대한 의존이 디커필링됨으로써 이 모듈을 독립적으로 가져다 쓰기도 쉽고 단위 테스트를 하기에도
훨씬 간단해졌습니다.
이전의 코드에선 test1.js 를 테스트하려해도 꼭 config.json 을 준비시켜야만 했는데, 이젠 적당히 fake config 객체를 넘김으로써
단위 테스트가 용이해진 것이죠.
어차피 app.js 에선 config 가 필요한데? 라고 하실 수도 있지만, 이 app.js 가 모듈이 실제로 생성되고 연결되는 것을 나타내는
부분입니다. 앱의 최상위 레이어이자 위에서 얘기했던 시스템의 모든 모듈 연결을 관리하는 컴퍼넌트라고 볼 수 있죠.
실제로 production 에서 사용되는 코드에선 이런 부분은 무조건 있어야 하니까요.
- 의존성 주입의 종류
const service = new Service(dependency1, dependency2);
위는 의존성 주입을 할 때 대부분 사용되는 케이스인 생성자 주입입니다. 객체를 생성하는 순간 전달되는 방식이며, 주입받은 객체가
변하지 않거나 반드시 필요한 경우 강제하기 위해 사용할 수 있습니다.
// test1.js
class A {
constructor(b) {
this.b = b;
}
a1() {
this.b.b1();
}
a2() {
console.info('a2');
}
}
module.exports = { A };
// test2.js
class B {
constructor(a) {
this.a = a;
}
b1() {
console.info('b1');
}
b2() {
this.a.a2();
}
}
module.exports = { B };
// app.js
const { A } = require('./test1');
const { B } = require('./test2');
const a = new A(null);
const b = new B(a);
b.a = a;
b.b2();
예시로 보이기 위해 좀 이상한 코드를 들고 왔는데.. 위는 또다른 의존성 인젝션인 속성 주입입니다.
뭐 거창한건 아니고 b.a = a 이 부분이 속성 주입을 하는 부분인데요. 코드에서 볼 수 있듯이 A 클래스는 B 클래스에 종속성을,
B 클래스는 A 클래스에 종속성을 갖고 있습니다. 이런 경우를 종속성 교착 상태라고 하며 서로에게 종속성이 있는 경우엔 인스턴스화할 수 없습니다. 따라서 new A(null) 로 만들어진 시점의 a 객체가 완전하진 않지만 일단 불완전한 객체를 먼저 생성한 후, b 객체를 생성하고
속성 주입을 통해 b 객체에 a 를 주입하는 방식입니다.
이런 특수한 케이스가 아니면 대부분 생성자 주입을 사용할 것을 권합니다. 위와 같은 경우에 속성 주입으로 해결을 하기 보다는,
뭔가 설계가 잘못되었을거라 보고 설계를 수정하는 것이 좋을 것 같습니다.
물론 이번 포스팅에서만 소개한 내용을 갖고 실제 프로젝트에 적용하려면, app.js 와 같은 부분에서 거의 모든 모듈을 다 들고있어야 해서
이게 정말 괜찮은건가? 할 수 있습니다. 연결해야 할 모듈이 많아지면 많아질수록 이러한 의구심과 함께 관리는 더 어려워질거구요.
이럴 경우 종속성 소유권을 분할하거나 (app.js 와 같은 하나의 컴퍼넌트가 다 하는 것이 아닌) 부분적으로 DI 를 적용할 수 있습니다.
또는 이제는 직접 구현할 필요가 없이 프레임워크가 대부분 해주긴 하지만 DI container 를 사용하는 것입니다. DI container 는 다른 포스팅에서 이어가도록 하겠습니다.
'Backend > Node.js' 카테고리의 다른 글
Node.JS 어플리케이션 확장 (0) | 2023.01.01 |
---|---|
Service Locator (서비스 로케이터) (0) | 2022.12.25 |
Use Github private packages (0) | 2022.11.20 |
모듈과 의존성 (0) | 2022.11.12 |
스트림 (Stream) (0) | 2022.09.03 |