
Node.js 디자인패턴을 읽으며 요약/정리한 내용입니다.
Chapter 01) Node.js 플랫폼에 대하여
개요
- Node.js의 철학 "Node way"
- Node.js 버전 6와 ES2015
- Reactor 패턴 - Node.js 비동기 아키텍처의 핵심 매커니즘
1.3 Reactor 패턴
TL;DR
- 비동기 특성의 핵심인 Reactor 패턴을 분석
- 단일 스레드 아키텍쳐, 논 블로킹 I/O와 같은 패턴의 기본 개념 확인
- Node.js 플랫폼 전체에 대한 기반을 형성하는 방법
- 샘플 코드는 대부분 의사코드로 작성됨
1.3.1 I/O는 속도가 느리다.
- 기본적으로 입/출력은 컴퓨터의 기본적인 동작 중에서 가장 느림
1.3.2 블로킹 I/O
- 함수 호출은 작업이 완료될 때 까지 스레드의 실행이 차단
data = socket.read();
print(data);
즉, 함수가 호출되면 해당 함수의 작업이 종료될 때 까지 다른 작업을 진행할 수 없음
- 웹 서버에서 동시성을 처리하기 위한 전통적인 접근방식: 멀티 스레드, 멀티 프로세스, 스레드 풀 사용
- 스레드는 시스템 리소스 측면에서 비용이 비싼 방법
1.3.3 논 블로킹 I/O
- 논 블로킹 I/O: 대부분의 최신 운영체제에서 지원하는 리소스를 액세스하는 매커니즘
- 해당 운영 모드에서 시스템 호출은 항상 즉시 반환
- 호출하는 순간에 결과를 사용할 수 없으면, 미리 정의된 상수를 반환하여 그 순간에 반환할 수 있는 데이터가 없음을 나타냄
- 논 블로킹 I/O에 엑세스하는 패턴
- busy-waiting: 루프 내에서 리소스를 적극적으로 폴링(polling)하는 것
- 데이터를 엑세스할 수 있는지 계속 물어보는(확인하는) 것
- 대부분 엄청난 양의 CPU 시간 낭비를 초래하는 패턴
resources = [socketA, socketB, pipeA];
while(!resources.isEmpty()) {
for(i = 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);
}
}
1.3.4 이벤트 디멀티플렉싱
- 동기 이벤트 디멀티플렉서 또는 이벤트 통지 인터페이스
- 최신 운영체제가 제공하는 효율적인 논 블로킹 리소스 처리를 위한 기본적인 매커니즘
- 감시된 일련의 리소스들로부터 I/O 이벤트를 수집하여 큐에 넣고 처리할 수 있는 새 이벤트가 있을 때까지 차단
socketA, pipeB;
watchedList.add(socketA, FOR_READ);
watchedList.add(pipeB, FOR_READ);
while(events = demultiplexer.watch(watchedList)) {
foreach(event in events) {
data = event.resource.read();
if(data === RESOURCE_CLOSED)
demultiplexer.unwatch(event.resource);
else
consumeData(data);
}
}
- 의사코드 해석
- 리소스를 데이터 구조(List)에 추가
- 이벤트 통지자에 감시할 리소스 그룹을 설정
- 해당 호출은 동기식, 감시 대상 자원 중 하나라도 읽을 준비가 될 때까지 차단
- 이벤트 디멀티플렉서에 의해 반환된 각 이벤트 처리
- 이 시점에서 각 이벤트와 관련된 리소스는 읽을 준비가 되어 있고, 차단되지 않는 상황이 보증됨
- 이벤트가 처리되고 나면, 다시 디멀티플렉서에서 처리 가능한 이벤트 발생까지 차단
- 이를 이벤트 루프(Event Loop)라고 함

- 하나의 스레드만 사용해도 다중 I/O 사용 작업을 동시에 실행 할 수 있는 능력을 손상시키지 않음
- 스레드가 아닌 시간에 따른 분산
- 이를 통해 프로세스 경쟁과 스레드 동기화 걱정이 없는 간단한 동시성 전략을 사용할 수 있음
1.3.5 Reactor 패턴 소개
- Reactor 패턴: 1.3.4에서 제시된 알고리즘에 특수화된 패턴
- 핵심 개념: 각 I/O 작업과 관련된 핸들러를 갖는 것, 이 핸들러는 이벤트가 생성되어 이벤트 루프에 의해 처리되는 즉시 호출
핸들러 => 특정 이벤트가 발생했을 때 실행될 함수 또는 기능, Node.js에서는 callback 함수로 표시됨

- Reactor 패턴을 사용하는 어플리케이션에서 발생하는 작업 도식
- 어플리케이션이 이벤트 디멀티플렉서에 요청을 전달하여 새로운 I/O 작업을 생성
- 처리가 완료될 때 호출될 핸들러도 지정
- 해당 작업은 논 블로킹 호출, 즉시 어플리케이션으로 제어 반환
- I/O 작업이 완료되면 이벤트 디멀티플렉서는 새 이벤트를 이벤트 큐에 추가
- 이벤트 루프가 이벤트 큐의 항목들에 대해 반복
- 각 이벤트에 대해 관련 핸들러가 호출됨
- 핸들러는 실행이 완료되면 이벤트 루프에 제어를 반환(5a), 그러나 실행 중 새로운 비동기 동작이 요청(5b)이 발생하면 이벤트 루프로 제어를 반환하기 전에 이벤트 디멀티플렉서에 추가될 수 있음
- 이벤트 큐 내의 모든 항목이 처리되면, 루프는 이벤트 디멀티플렉서에서 다시 블록, 처리 가능한 이벤트 생성 시 해당 과정이 트리거
Node.js 어플리케이션은 이벤트 디멀티플렉서에 더 이상 보류 중인 작업이 없고, 이벤트 큐에서 더 이상 처리할 이벤트가 없을 때 자동으로 종료
1.3.6 Node.js의 논 블로킹 엔진 libuv
- 각 운영체제에는 이벤트 디멀티플렉서에 대한 자체 인터페이스가 있음
- Linux=epoll, MacOS=kqueue, Windows=I/O Completion Port(IOCP) 등
- libuv: Node.js 코어 팀이 각 운영체제의 다른 특성으로 인해 이벤트 디멀티플렉서에 대한 높은 추상화가 요구되고, 이를 C 라이브러리로 구현
- 모든 주요 플랫폼과 호환, 서로 다른 유형의 리소스들의 동작을 표준화
- 오늘날 libuv는 Node.js의 하위 수준의 I/O 엔진을 말함
1.3.7 Node.js를 위한 구조
- 리액터 패턴과 libuv가 Node.js의 기본 구성 요소
- 전체 플랫폼을 위해서는 다음 세가지 구성 요소가 필요
- 바인딩 세트: libuv와 기타 낮은 수준의 기능을 JavaScript에 랩핑하고 사용 가능하게 해줌
- V8: 크롬 브라우저 용으로 개발한 JavaScript 엔진
- 코어 JavaScript API: 상위 수준의 Node.js API를 구현, 노드 코어라고도 함
