2장 (1/3): CPS 패턴

이 글은 CPS 패턴과 CPS가 Node.js에서 어떻게 사용되고, 어떤 점을 주의해야 하는지 다룬다.


1. CPS 패턴

Node.js는 1장에서 살펴봤듯 비동기 특성을 가지며, 따라서 Node.js 앱은 대부분의 일을 비동기로 처리할 수 밖에 없다. 비동기를 처리하는 방법 중 CPS, Continous Passing Style을 소개한다.

CPS: 비동기 API를 사용할 때, 콜백 함수를 인자로 넘기는 패턴이다.

  • 왜 사용하는가: 비동기 API는 return을 할 수 없는데, 함수의 실행이 끝나기 전에 제어권이 넘어가기 때문이다. 이를 해결하기 위해선 결과를 다른 함수에 넘기면 된다.
  • 장점: 간단하고 효과적이다.
  • 단점: 호출 깊이가 깊어지면 가독성이 감소된다. Callback Hell이라고 불린다.

2. Node.js에서의 CPS 패턴

Node.js는 CPS 패턴을 사용할 때 일관된 규칙을 따라야 한다.

  • argument 순서에 관한 규칙: (...params, callback) 과 같이, callback 함수를 마지막 인자로 넘겨야 한다.
  • callback 함수의 argument에 관한 규칙: (err, ...args) 와 같이, err가 첫 인자여야 한다.
  • err 인자의 경우, 항상 Error() 객체여야 한다. (이 부분은 잘 지켜지지 않는 듯 하다.)

3. CPS 패턴의 콜 스택

Node.js에서 비동기 API를 호출하는 경우, callback 함수는 프로그래머가 예상한 호출 순서로 구성된 스택을 갖지 않는다. 비동기 API가 완료됐을 때, 이벤트 루프에 의해 단일 함수로 Queue에 쌓인 후 다른 타이밍에 실행되기 때문에 새로운 스택에서 실행된다. 비동기 함수에서 예외를 던지면, Error를 반환하며 프로세스가 종료된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const fs = require('fs');
const readJsonThrows = (filename, cb) => {
try {
fs.readFile(filename, 'utf8', (err, data) => {
if (err) return cb(err);
cb(null, JSON.parse(data));
// cb이 없거나, data가 불량인 경우 exception 발생 가능.
// 콜백 함수 내에서 try-catch하지 않는 경우 프로세스가 죽는다.
});
} catch (err) {
// 여기서도 catch할 수 없음. 호출 스택은 fs.readFile에서 끝나고,
// cb은 별개의 새 스택에서 실행되기 때문
}
};

// 만약 JSON.parse에서 오류나는 경우, 프로세스가 종료된다.
readJsonThrows('C./test.json', (f) => f);
/*
SyntaxError: Unexcepted end of JSON input
at JSON.parse
at FSReqCallback.readFileAfterClose (internal/...)
*/

4. Node.js에서 비동기를 처리할 때 절대 하지 말아야 할 점들

1. 결괏값을 동기, 비동기 2가지 방식으로 전달하지 않는다.

  • 결괏값이 비동기일것을 기대하고 이벤트 리스너를 등록할 때, 동기로 결괏값이 제공되는 경우 이벤트 리스너가 동작하지 않는다.

  • 동기 반환값을 비동기화 한다. setTimeout, setImmediate, nextTick, Promise 등이 가능하다.

2. Callback 함수를 argument로 받는 동기 함수를 작성하지 않는다.

  • 동기 API는 바로 결괏값을 받는 형태로 코드를 작성하면 된다.

1장: Reactor 패턴 (내용 검증 필요)

1장은 Node.js에 대한 소개하는 챕터이다.


주의!
해당 글의 내용은 부정확한 내용이 아주 많을 수 있습니다. 책의 설명이 추상적이고 OS 개념이 많이 필요하므로 추후 정리가 완료되는 경우 따로 표시하겠습니다.


Node.js 철학 (Node Way)

최소 기능: 기능 개수를 최소한되므로, 개발자, 사용자 모두에게 간단함

  1. Node.js 자체 뿐만 아니라 node 기반 모듈을 설계할 때도 동일하게 적용
  2. KISS 원칙: 부족하더라도 복잡함보다 단순함이 더 낫다

Reactor 패턴과 Node.js 이벤트 루프

Reactor 패턴은 Node.js의 비동기 특성 - Node.js에서 여러 요청이 동시에 있는 경우는 항상 비동기 방식으로 작업을 처리한다 - 의 원인이자, 비동기 방식으로 작업을 처리하는 방법에 해당한다. Reactor 패턴을 배우기 전에, 동시성을 처리하는 2가지 방법에 대해서 알아보자.

Blocking I/O : 느린 I/O기다리는 방식

  1. 많은 스레드 개수: 소켓의 데이터를 매번 기다리게 되면 각 연결 별로 스레드가 적어도 하나씩 돌아야 한다. 기다리는 시간에 타 사용자가 기다리지 않게 하기 위해서이다.

  2. 비효율적인 대기 시간: I/O가 CPU에 비해 매우 느리기 때문에 블로킹 API는 스레드의 유휴 시간이 처리 시간에 비해 압도적으로 길 수 밖에 없다. 스레드가 아무 일을 하지 않은 상태로 긴 시간 존재한다.

  3. 스레드의 비용: 스레드는 그 비용이 싸지 않다. 아주 많은 스레드가 있는 경우, Context Switching만 해도 비용이 매우 클 것이고, 적은 스레드가 있는 경우 사용자를 처리하지 못하므로 비즈니스적으로 비용이 매우 클 것이다.

Non-blocking I/O: 비동기 API를 호출 시 바로 제어권을 반환(내부적으로 특정 상수를 반환)하여 CPU 유휴 시간을 최소화한다.

  1. Polling: 비효율적으로 I/O를 처리하는 방식으로, 리소스는 데이터가 없을 때 읽기 조작을 요청 받는 경우 EAGAIN을 반환하는데, 이 때문에 값이 필요한 입장에선 리소스를 계속 확인해야 한다. 이걸 BUSY_WAITING이라고 하는데, CPU를 계속 활용하므로 효율적이지 못하다.

  2. 동기 이벤트 디멀티플렉서: 논블로킹을 처리하는 효율적인 방법으로, 이벤트가 완료될 때마다 큐에 이벤트를 쌓아놓고 처리를 수행하는 객체. 이벤트가 없으면 Block 상태로 대기한다.

    1. 이벤트 통지자가 감시 대상 리소스의 자원이 읽기가 가능할 때(즉, 이벤트가 완료되었을 때) Demultiplexer에게 통지한다. (이벤트 통지자 역할로 IOCP, epoll/kqueue 등이 있는 것 같다.)

    2. Event가 발생하면 Event Demultiplexer가 깨어나 Queue에서 이벤트를 읽어들여 처리하면 됨. 이 시점에서 리소스의 I/O 작업은 (1)에서 이미 완료되어있으므로 동기식으로 처리하면 됨. 또한 처리 방식이 싱글 스레드이므로 공유 자원 문제도 존재하지 않는다.

리액터 패턴: 이벤트 디멀티플렉서 + 이벤트 루프 + 이벤트 큐 + 실행 환경(V8, 싱글 스레드!)

  1. 이벤트 디멀티플렉서는 I/O 처리가 끝나면 (완료된) 이벤트를 이벤트 큐에 넣어줌

  2. 이벤트 루프는 실행 환경 상에서 스택이 비는 경우(즉 모든 동기 코드가 실행이 끝났을 때 - 노드 환경에서 동기 코드는 얼마 없어서 최초의 동기 코드는 금방 끝나기 마련.), 이벤트 큐에서 이벤트를 꺼내어 실행 환경에 이벤트 핸들러를 올리고, 인자로 이벤트를 넘겨 수행함.

    1. 만약 async 내에 async가 있다면 해당 이벤트는 또 이 과정을 거침.

이벤트 디멀티플렉서의 구현체

libuv: 크로스 플랫폼으로(가상머신 느낌으로 각 OS에 대응되는 이벤트 통지자를 활용) 비동기 작업을 처리함. 단, libuv는 이벤트 디멀티플렉서 역할만 하는 게 아니라 이벤트 루프도 구현함.

참고: libuv에 이벤트루프가 포함돼있음:

더 정확한 이벤트 디멀티플렉서, 이벤트 루프 구현 상세에 관한 글, 영상

로우 레벨로 살펴보는 Node.js 이벤트 루프 | Evans Library

Node.js 이벤트 루프, 타이머, process.nextTick() | Node.js (놀랍게도 이 문서가 더 어려운 것 같다…)

브라우저 환경에서의 이벤트 루프(자막 있음, 자세함!):
Jake Archibald: In The Loop | JSConf.Asia

아마도 이벤트 루프에 대한 가장 유명하고, 쉬운 설명:
What the heck is the event loop anyway? | JSConf EU