[Node.js] 이벤트 루프 (Event Loop)

Blue Whale·2021년 9월 22일
0

Javascript

목록 보기
2/3

Node.js

Nodejs는 v8 엔진 기반event-driven, non-blocking, single-threaded 자바스크립트 런타임이다. 이러한 nodejs의 특징을 설명할 때, 빠트릴 수 없는 것이 바로 Event Loop 이다. Event Loop는 Nodejs의 작동 원리를 관통하는 핵심 주제이다. 따라서, 이번 기회에 이를 확실히 정리하고자 한다.

Event Loop의 구조

디자인 패턴 중 하나인, Reactor Pattern에서는 다양한 이벤트와 이에 대한 반응의 형태를 정의한다. Nodejs의 이벤트 루프도 큰 흐름에서는 이와 유사하다. 아래의 사진은 nodejs 공식문서에서 제공하는 이벤트 루프의 구조이다.

Nodejs의 이벤트 루프는 다양한 이벤트(ex, Timer, I/O, close)를 단계별로 나누고 각 단계에서 관리하는 이벤트 큐에 저장한다. 그리고, Nodejs는 해당 단계의 큐에 쌓인 콜백을 모두 소진하거나 최대 실행 가능한 개수 만큼의 콜백을 실행한 후에 다음 단계로 이동한다. 아래의 사진은 각 단계를 공식문서에서 간략히 설명한 것이다.

Timer

이벤트 루프의 가장 앞에 위치한 setTimeout, setInterval 함수에 의해 타이머들을 확인하는 단계이다. 여담으로, 한 가지 흥미로운 점은 타이머는 내부적으로는 큐가 아니라 min-heap으로 느슨하게 관리되고 있다는 것이다. 이 단계에서는 타이머가 만료된 콜백들이 실행되며, 이어서 설명할 poll 단계에서 큐가 비어있는 경우에도 timer 단계를 다시 한번 체크한다.

Pending Callbacks

이전 루프를 순회하는 과정에서 등록된 I/O 콜백들을 실행하는 단계이다. 주로, 이전 루프에서 실행한 I/O 작업의 콜백이나 에러 핸들러 콜백들을 실행한다.

Idel, Prepare

Nodejs 내부 관리를 위한 단계로, 사용자가 어플리케이션 레벨에서 고려하진 않는다.

Poll

Poll 단계는 파일 시스템 I/O, 네트워크 I/O 을 마치고 실행될 콜백들을 처리하는 단계이다. 이벤트 루프가 Poll 단계에 들어서면 poll 큐의 상태에 따라 다른 행동을 취한다.

  • 만약 poll 큐가 비어있지 않은 경우에는 시스템 제한 범위 내에서 큐에 쌓인 콜백을 모두 순차적으로 처리한다.
  • poll 큐가 비어있는 경우에는, 다음 중 하나의 작업을 수행한다.
    • setImmediate() 함수로 스케쥴링된 콜백이 존재하는 경우에는, poll 단계를 종료하고, 이를 전담하여 처리하는 check 단계로 넘어간다.
    • 만약, check 단계에서 처리할 콜백이 없는 경우에는 timer 큐를 확인한다. 만약, timer 큐가 차있는 경우에는 해당 콜백이 실행될 수 있는 시간 만큼을 대기하고 다음 단계로 넘어간다. 만약. tiemr 큐마저 비어있는 상태인 경우에는, 새로운 이벤트(ex, incoming connection)를 수신할 때 까지, poll 단계에서 잠시 대기하게 된다.

Check Phase

setImmediate()에 의해 스케쥴링된 콜백들을 처리하는 특수한 단계이다.

Close callbacks

이벤트 루프의 마지막 단계로, 소켓 연결 종료(socket.destroy())와 같은 close 이벤트와 관련된 콜백들을 처리하는 단계이다. timer 단계에서 수행할 콜백이 있는 경우에는 이벤트 루프를 다시 순회한다. 그렇지 않은 경우, 이벤트 루프는 종료된다.

nextTickQueue

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

microTaskQueue는 기능적으로 nextTickQueue와 유사하지만, Promise의 콜백을 처리한다는 점에서 차이가 있다. 또한, microTaskQuq는 nextTickQueue보다 낮은 우선순위를 갖는다.

libuv

서로 다른 OS에서 지원하는 모든 I/O를 처리하기란 결코 쉽지 않다. I/O 구현을 위한 방법도 OS 계열별로 상이(ex, epoll, kqueue, IOCP)할 뿐만 아니라 매우 복잡하다. 몇몇 OS의 일부 기능은 비동기 I/O를 지원하지 않는 경우도 있다. 따라서, nodejs에서 효과적으로 완전한 비동기 I/O를 지원하기 위해서는 이러한 복잡성을 추상화하고 비동기 I/O를 지원하는 계층을 필요로 하였다. 이를 위해 nodejs가 채택한 것이 바로 libuv이다.다.

References

profile
안녕하세요

0개의 댓글