Nodejs는 v8 엔진 기반
의 event-driven
, non-blocking
, single-threaded
자바스크립트 런타임이다. 이러한 nodejs의 특징을 설명할 때, 빠트릴 수 없는 것이 바로 Event Loop
이다. Event Loop
는 Nodejs의 작동 원리를 관통하는 핵심 주제이다. 따라서, 이번 기회에 이를 확실히 정리하고자 한다.
디자인 패턴 중 하나인, Reactor Pattern
에서는 다양한 이벤트와 이에 대한 반응의 형태를 정의한다. Nodejs의 이벤트 루프도 큰 흐름에서는 이와 유사하다. 아래의 사진은 nodejs 공식문서에서 제공하는 이벤트 루프의 구조이다.
Nodejs의 이벤트 루프는 다양한 이벤트(ex, Timer
, I/O
, close
)를 단계별로 나누고 각 단계에서 관리하는 이벤트 큐에 저장한다. 그리고, Nodejs는 해당 단계의 큐에 쌓인 콜백을 모두 소진하거나 최대 실행 가능한 개수 만큼의 콜백을 실행한 후에 다음 단계로 이동한다. 아래의 사진은 각 단계를 공식문서에서 간략히 설명한 것이다.
이벤트 루프의 가장 앞에 위치한 setTimeout
, setInterval
함수에 의해 타이머들을 확인하는 단계이다. 여담으로, 한 가지 흥미로운 점은 타이머는 내부적으로는 큐가 아니라 min-heap
으로 느슨하게 관리되고 있다는 것이다. 이 단계에서는 타이머가 만료된 콜백들이 실행되며, 이어서 설명할 poll 단계에서 큐가 비어있는 경우에도 timer 단계를 다시 한번 체크한다.
이전 루프를 순회하는 과정에서 등록된 I/O 콜백들을 실행하는 단계이다. 주로, 이전 루프에서 실행한 I/O 작업의 콜백이나 에러 핸들러 콜백들을 실행한다.
Nodejs 내부 관리를 위한 단계로, 사용자가 어플리케이션 레벨에서 고려하진 않는다.
Poll 단계는 파일 시스템 I/O
, 네트워크 I/O
을 마치고 실행될 콜백들을 처리하는 단계이다. 이벤트 루프가 Poll 단계에 들어서면 poll 큐의 상태에 따라 다른 행동을 취한다.
setImmediate()
함수로 스케쥴링된 콜백이 존재하는 경우에는, poll 단계를 종료하고, 이를 전담하여 처리하는 check 단계로 넘어간다.incoming connection
)를 수신할 때 까지, poll 단계에서 잠시 대기하게 된다.setImmediate()
에 의해 스케쥴링된 콜백들을 처리하는 특수한 단계이다.
이벤트 루프의 마지막 단계로, 소켓 연결 종료(socket.destroy()
)와 같은 close
이벤트와 관련된 콜백들을 처리하는 단계이다. timer 단계에서 수행할 콜백이 있는 경우에는 이벤트 루프를 다시 순회한다. 그렇지 않은 경우, 이벤트 루프는 종료된다.
nextTickQueue
단계는 엄밀히 이벤트 루프에 속하지는 않지만, 이벤트 루프에 진입하거나, 매 단계가 끝날 때마다 처리되는 상당히 큐이다. nextTickQueue에는 nextQueue process.nextTick()
API에 의해 등록된 콜백들이 저장된다. Nodejs는 이벤트 루프의 한 단계를 마치면, nextTickQueue에 등록된 모든 콜백을 처리하고 다음 단계로 넘어간다. 따라서, nextTickQueue에 등록된 콜백이 너무 많은 경우에는 이벤트 루프가 다음 단계로 넘어가지 못 하는 I/O Starvation
이 발생할 수 있다.
이러한 단점에도 불구하고 nextTickQueue가 특정 시점(ex, 스택이 모두 처리된 경우)에만 실행된다는 점을 활용하여 유용하게 활용된다. nextTickQueue를 활용하면 코드의 지연 실행(defer
)이 가능하다.
let bar;
function someAsyncApiCall(callback) {
process.nextTick(callback);
}
someAsyncApiCall(() => {
console.log('bar', bar); // 1
});
bar = 1;
const server = net.createServer(() => {}).listen(8080);
server.on('listening', () => {});
microTaskQueue
는 기능적으로 nextTickQueue와 유사하지만, Promise
의 콜백을 처리한다는 점에서 차이가 있다. 또한, microTaskQuq는 nextTickQueue보다 낮은 우선순위를 갖는다.
서로 다른 OS에서 지원하는 모든 I/O를 처리하기란 결코 쉽지 않다. I/O 구현을 위한 방법도 OS 계열별로 상이(ex, epoll
, kqueue
, IOCP
)할 뿐만 아니라 매우 복잡하다. 몇몇 OS의 일부 기능은 비동기 I/O를 지원하지 않는 경우도 있다. 따라서, nodejs에서 효과적으로 완전한 비동기 I/O를 지원하기 위해서는 이러한 복잡성을 추상화하고 비동기 I/O를 지원하는 계층을 필요로 하였다. 이를 위해 nodejs가 채택한 것이 바로 libuv
이다.다.
yceffort
님의 Nodejs의 이벤트 루프 살펴보기