Vana Blog

Node.js Design Pattern - Node.js 철학과 특징, Reactor패턴

March 22, 2019

node.js의 철학

경량 코어

코어를 최소의 기능 세트로 하고 나머지는 사용자의 몫으로 한다.

경량 모듈

작은 모듈을 설계하는 것이 원칙이다. 이는 유닉스 철학에서 왔다.

작은 것이 아름답다. 각 프로그램이 각기 한 가지 역할을 잘 하도록 만든다.

경량 모듈의 장점

  1. 이해하기 쉽고 사용하기 쉽다.
  2. 테스트 및 유지보수가 훨씬 간단하다.
  3. 브라우저와 완벽한 공유가 가능하다.

** 재사용성을 강조 DRY(Don’t Repeat Yourself)

코어 모듈, 파일 모듈, npm 모듈

참고 : 노드 API

  • 모듈은 대부분 객체를 내보내지만 함수 하나만 exprot할 때도 있다. 하나만 내보내는 경우는 그 모듈의 함수를 즉시 호출하려는 의도로 만들 때 주로 사용한다.
  • 노드는 노드 앱을 실행할 때 어떤 모듈이든 단 한 번만 import한다.

node.js 특징

  • 싱글 스레드
  • 비동기 I/O
  • 이벤트 기반(event driven)

4버전 이후에는 ES2015 사양이 도입된 기능을 지원. let, const 키워드 사용가능 등

** 사실 ES6에서 const에 바인딩 된 값은 상수가 되나 할당된 값이 상수가 된다는 것은 아니다.

const x = 'this will never change';
= '...' // error
 
const x = {};
x.name = 'vana'; // 에러 아님

객체 내부에서 속성을 변경하면 실제 값이 변경되나 변수와 객체 사이의 바인딩은 변경되지 않으므로 해당 코드는 오류를 발생하지 않는다.

반대로 전체 변수를 재할당하면 변수와 값 사이의 바인딩이 변경되어 오류가 발생한다.

x = null; // 오류 발생

tip. 불변 객체를 만들고 싶다면 const만으로는 충분하지 않기 때문에 ES5의 Object.freeze()메소드 또는 deep-freeze모듈을 사용해야 한다.

화살표 함수에서 this의 범위는?

화살표 함수는 어휘 범위(lexical scope)로 바인딩 된다.

즉 화살표 함수 내부의 this값은 부모 블록의 값과 같다.

function DelayedGreeter(name) {
  this.name = name;
}
 
DelayedGreeter.prototype.greet = function() {
  setTimeout(function cb() {
    console.log('Hello, ' + this.name);
  }, 500);
};
 
const greeter = new DelayedGreeter('World');
greeter.greet(); // Hello, undefined

위의 예제와 같은 결과가 나오는 이유는 5초 뒤에 실행되는 cb함수 내부의 함수 scope이 greet메소드의 scope과 다르기에 같은 this를 공유할 수 없기 때문이다. 이 문제를 해결하기 위해 이전에는 바인딩을 사용해서 처리했다.

DelayedGreeter.prototype.greet = function() {
  setTimeout((function cb() {
    console.log('Hello, ' + this.name);
  }).bind(this), 500);
}

화살표 함수가 도입되면서 어휘 범위에 바인딩 되기 때문에 화살표 함수를 콜백함수로 사용하여 문제를 해결할 수 있게 되었다.

DelayedGreeter.prototype.greet = function() {
  setTimeout( () => console.log(`Hello, ${this.name}`), 500);
};

파일 시스템 접근

참고 : fs API

node가 만들어지기 전에는 javascript는 파일시스템에 접근 할 수 없었다.

파일 쓰기

const fs = require('fs');
 
fs.writeFile(path/파일명, 내용, function(err) {
  error callback function
})

파일 읽기

const fs = require('fs');
 
fs.readFile(path/파일명, {encoding: 'utf8'}, function(err, data) {
  error callback function
})
  • fs.readdir : 디렉토리에 어떤 파일이 있는지 알아보는 함수
  • fs.unlink : 파일 지우기
  • fs.rename : 파일을 옮기거나 이름을 바꿈.
  • fs.stat : 파일과 디렉토리 정보

Reactor 패턴

블로킹 I/O

// 데이터를 사용할 수 있을 때까지 스레드가 블록된다.
data = socket.read();
print(data);

thread를 이용하는 방법은 자원을 많이 사용하기 때문에 최상의 방법이라 할 수 없다.

논 블로킹 I/O

논 플로킹 I/O에 엑세스하는 가장 기본적인 패턴 예제. 실제 데이터가 반환될 때까지 루프 내에서 리소스를 적극적으로 polling하는 것이다. 이것을 busy-waiting이라고 한다.

resources = [socketA, socketB, pipeA];
while(!resources.isEmpty) {
  for(= 0; i < resources.length; i++) {
    resource = resources[i];
 
    let data = resource.read();
    if(data === NO_DATA_AVAILABLE) // 당장 읽을 데이터가 없을 경우
      continue;
    if(data === RESOURCE_CLOSED) // 데이터 리소스가 닫혔기 때문에 리소스 목록에서 제거
      resources.remove(i);
    else // 데이터가 도착하여 이를 처리
      consumeData(data);
  }
}

위 예제는 동일한 스레드에서 서로 다른 리소르를 처리할 수 있지만 리소스를 반복하는데 CPU를 많이 사용하여 효율적이지 않다. 폴링 알고리즘은 대부분 엄청난 양의 CPU 시간 낭비를 초래한다.

이벤트 디멀티플렉싱

대부문 최신 운영체제는 효율적인 논 블로킹 리소스 처리를 위한 기본적인 메커니즘을 제공한다.

이 메커니즘을 동기 이벤트 디멀티플렉서 또는 이벤트 통지 인터페이스라고 한다. 감시된 일련의 리소스들로부처 들어오는 I/O 이벤트를 수집하여 큐에 넣고 처리할 수 있는 새 이벤트가 있을 때까지 차단한다. 다음은 두 개의 서로 다른 자원에서 읽기 위해 일반 동기 이벤트 디멀티플렉서를 사용하는 알고리즘의 의사 코드이다.

socketA, pipeB;
watchedList.add(socketA, FOR_READ);
watchedList.add(pipeB, FOR_READ);
while(evnets = demultiplexer.watch(watchedList)) {
  foreach(event in events) {
    data = event.resource.read();
    if(data === RESOURCE_CLOSED)
      demultiplexer.unwatch(event.resource);
    else
      consumeDta(data);
  }
}

위 패턴은 Busy-waiting을 사용하지 않고도 단일 스레드 내에서 여러 I/O 작업을 처리할 수 있다.

Reactor 패턴 소개

패턴은 일련의 관찰 대상 리소스에서 새 이벤트를 사용할 수 있을 때까지 차단하여 I/O를 처리한 다음, 각 이벤트를 관련 핸들러로 전달함으로써 반응한다.

  1. 어플리케이션 -> 이벤트 디멀티플렉서에 요청 전달함으로써 새로운 I/O작업 생성. 어플리케이션은 처리가 완료될 때 호출될 핸들러를 지정. 이벤트 디멀티플렉서에 새 요청을 전달하는 것은 논 블로킹 호출이며 즉시 어플리케이션에 제어를 반환한다.
  2. 일련의 I/O 작업들이 완료되면 이벤트 디멀티플렉서는 새 이벤트를 이벤트 큐에 집어넣는다.
  3. 이 시점에서 이벤트 루프가 이벤트 큐의 항목들에 대해 반복한다.
  4. 각 이벤트에 대해서 관련된 핸들러가 호출된다.
  5. 어플리케이션 코드의 일부인 핸들러는 실행이 완료되면 이벤트 루프에 제어를 되돌린다(5a), 그러나 핸들러의 실행 중에 새로운 비동기 동작이 요청(5b)이 발생하여 제어가 이벤트 루프로 돌아가기 전에 새로운 요청이 이벤트 디멀티플렉서(1)에 삽입될 수도 있다.
  6. 이벤트 큐 내의 모든 항목이 처리되면, 루프는 이벤트 디멀티플렉서에서 다시 블록되고 처리 가능한 새로운 이벤트가 있을 때 이 과정이 다시 트리거 될 것이다.

Vana Yun

Written by Vana Yun