본문 바로가기

Backend/Node.js

모듈과 의존성

이번 포스팅부터 몇개의 글에서는 Node.JS 의 모듈과 의존성(=종속성)에 대해 남겨보고자 합니다.

 

Node.JS 에서 가장 흔하게 사용하는 모듈 시스템은 CommonJS 모듈 시스템입니다.

모듈은 코드를 구성하고 구조화하는데 가장 기본적인 방식이며, Node.JS 개발을 하다보면 모듈화 하는것에 꽤나 많은 시간을

쏟게 되는 것 같습니다. 이는 결국 OOP 에서 객체를 나누는 일이나, layered architecture 에서 계층을 나누는 것과 비슷한

맥락이라고 생각합니다.

 

- 응집력, 결합력

 

소프트웨어 개발에서 가장 흔하게 나오는 응집력, 결합력은 모듈을 만들 때도 동일합니다.

응집력은 높을수록 좋습니다. 응집도가 높다는 것은 어떠한 기능을 하기 위한 책임, 로직들이 얼마나 모여있는가를 말합니다.

어떤 모듈이 단 하나의 일만 한다면, 이는 굉장히 높은 응집력을 갖는다고 할 수 있습니다.

 

class A {
  public method1() {
    ...
    this.method2();
  }
  
  private method2() {
    ...
  }
  
  public method3() {
    ...
    this.method4();
    this.method5();
  }
  
  private method4() {
    ...
  }
  
  private method5() {
    ...
  }
}

 

모듈 얘기하고있는데 좀 이상한 예시이긴 합니다만.. 응집력은 어떤 클래스의 메서드 순서를 나열하는 부분에서도 언급되곤 합니다.

일반적으로 public -> private 순서로 메서드를 나열하곤 합니다만, 위처럼 public method1 에서는 private method2 를 호출하는
경우엔 묶어두는 것이 코드를 읽는데도 훨씬 도움이 되겠죠. 응집력이 낮으면 가독성이 좋지 않고, 재사용이 어렵습니다.

 

반대로 결합력은 낮을수록 좋습니다. 결합력은 어떤 요소가 다른 요소에 얼마나 의존하는지를 말하고, 이 의존 정도가 강하면 강결합이
있다고 얘기합니다. 이런 결합력이 강하면 어떤 기능을 수정할 때 의존하는 다른 부분도 수정해야 할 확률이 높고, 기능에 대해 파악할 때
의존하는 부분까지 파악해야 하는 경우가 생깁니다. 결국 그 단점은 응집력이 낮았을 때와 비슷하겠죠.

 

따라서 우리는 모듈을 만들 때 모듈의 응집력은 높이고 결합력은 낮춤으로써 그 모듈이 쉽게 재사용되고, 가독성이 높으며
유지보수하기 쉽도록 만들고자 합니다.

 

- Node.JS 의 싱글톤

 

역시나.. 허접한 예시를 하나 들어보겠습니다.

 

// test-module.js
class TestModule {
  constructor() {
    console.info('create');
  }

  method1() {
    console.info('method1');
  }
}

module.exports = new TestModule();

// test1.js
const testModule = require('./test-module');

module.exports = {
  run: () => {
    testModule.method1();
  },
};

// test2.js
const testModule = require('./test-module');

module.exports = {
  run: () => {
    testModule.method1();
  },
};

// test3.js
const test1 = require('./test1');
const test2 = require('./test2');

test1.run();
test2.run();
/*
  create
  method1
  method1
*/

 

test-module.js 에선 TestModule 객체를 생성해 module.exports 로 내보내고 있습니다. test1, test2 에선 그 객체를 사용하는데
실행된 코드를 보면 의도한대로 TestModule 객체는 한번만 생성이 됐습니다. 우리는 이를 통해 아주 간단하게 Node.JS 모듈에서
싱글톤을 구현할 수 있다는 걸 알 수 있습니다.

 

Node.JS 는 require() 의 첫 호출 이후 모듈을 캐시하고, 이후 호출에선 다시 실행하는 것이 아닌 캐시된 것을 반환하는 형태로 동작하기

때문에 TestModule 객체가 한번만 생성된다는 것이 가능해집니다. 다만 모듈의 캐시는 현재 패키지 내에서만 보장됩니다.

만약 위와 같은 코드가 node_modules 의 A 패키지, B 패키지에서 따로 호출되고 있었다면 TestModule 객체는 더이상 싱글톤이라고

할 수 없지만, 우리가 직접 작성하는 코드라는 가정하에선 싱글톤이라고 봐도 무방합니다.

 

- 의존성 하드코딩

 

// 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...

 

또 허접한 예시를 들고 왔습니다. 위 예시에선 require 를 통해 test1.js 에서 config 모듈을 불러와 사용하는 형태를 보여주고 있으며,

Node.JS 에서 이는 너무도 흔한 사용법입니다. 그렇지만 이런걸 의존성 하드코딩이라고 부르는데, 이런 설정 방법은 너무 간단하고
효과적이지만 모듈의 재사용을 제한합니다.

 

위 코드에선 test1.js 는 config.js 에 종속되고, config.js 는 config.json 에 종속됩니다.

config.json 파일이 있다면 문제는 없겠지만, config.json 파일이 없다면 어떨까요? test1.js 는 config.json 때문에 실패합니다.

물론 이것이 서버의 일부 코드라고 했을 때, 개발자는 당연히 서버가 config.json 을 정상적인 위치에 두고 실행되도록 작업하겠지만,
이렇게 종속성이 하드코딩으로 묶여있으면 재사용성이 낮아지고 테스트가 어려워집니다.

일단 위 코드는 만약 test1 을 테스트를 돌리려한다면 테스트 코드가 돌아갈때도 config.json 을 넣어둬야겠죠.

 

Node.JS 에서 require 의 사용은 직관적이라 이해하기 쉽고 사용도 편리하지만, 이런 식의 상태가 있는 모듈을 불러오는 부분에선

꽤나 단점으로 다가오게 됩니다. 이 말은 반대로하면 상태가 없는 모듈은 require 로 로드할 땐 아무런 문제가 없고, 모듈의 재사용성에
별 영향을 주진 않습니다. 어찌됐건 하드코딩된 의존성에 의한 강결합 이라는 단점 때문에 DI 패턴 이라는 것을 사용해볼 때가 되었습니다.
이는 다음 포스팅에서 보도록 하겠습니다.

'Backend > Node.js' 카테고리의 다른 글

의존성 주입  (0) 2022.12.10
Use Github private packages  (0) 2022.11.20
스트림 (Stream)  (0) 2022.09.03
제네레이터 (Generator)  (0) 2022.08.27
Promise & Promisify  (0) 2022.08.20