Node.js로 서버를 굴리고 있는 나지만, 정작 내가 Node.js를 이해하고 사용하는지는 항상 의문이었다.
그에 따라 Node.js의 핵심 요소 중 하나인 Event Loop를 공부해 본다.
# Event Loop란?
참조 : Node.js 공식문서
위 다이어 그램의 각 박스는 특정 작업을 수행하기 위한 이벤트 루프의 페이즈를 의미한다.
긱 페이즈 마다 콜백을 실행할 FIFO 큐를 가지며, 각 단계에서 정해진 역할만 수행 후 다음 페이즈로 이동한다.
(다음 페이즈로 이동하는 규칙은 해당 페이즈의 큐를 모두 소진하거나, 콜백의 최대 개수를 실행했을 때이다.)
실제로 7~8개의 페이즈가 있지만,
Node.js에서 실제로 신경 써야 하는 가장 중요한 부분은 위 다이어그램에 속한 6단계의 페이즈들이다.
1. timers 페이즈
timers 페이즈는 이벤트 루프의 시작 단계로, 흔히 사용하는 setTimeout, setInterval 같은 타이머 함수들의 콜백을 저장하는 큐를 가지고 있다.
물론, 이 페이즈에서 모든 콜백이 바로 큐에 들어가는 것은 아니고 min-heap으로 타이머들을 유지하고 있다가 마침 실행할 때가 되었을 경우, 그 콜백을 heap에서 꺼내어 큐에 넣고 실행을 한다.
여기서 함정은 타이머 함수의 두번째 인자인 ms 임계값이 정확히 실행됨을 보장하지 않는다는 것이다.
운영체제 스케쥴링 혹은 다른 페이즈의 콜백 실행으로 인해 임계값보다 지연되어 실행될 가능성이 농후하다.
2. pending callbacks 페이즈
pending callbacks 페이즈에서는 이전 루프에서 연기된 I/O 콜백 혹은 TCP 오류와 같은 시스템 작업의 콜백을 실행하며,
유닉스 계열의 통신 에러인 ECONNREFUSED와 같은 이벤트 발생시의 에러 핸들러 콜백 또한 이곳에서 처리한다.
(따라서, 이 큐에 존재하는 콜백들은 이전 루프에서 이미 큐에 들어온 콜백들이다.)
3. idle, prepare 페이즈
이 페이즈는 내부용으로만 사용합니다... 라는 공식문서의 내용으로 인해 구글링을 해보았는데,
이벤트 루프와 직접적인 관련이 있기 보다 Node.js의 내부적인 관리를 위한 것이라고 하니 일단 넘어 간다.
4. poll 페이즈
poll 페이즈는 이벤트 루프의 핵심 페이즈이다.
close이벤트 콜백, 타이머 함수로 스케줄 된 콜백, setImmediate를 제외한 거의 모든 콜백들을 이 페이즈에서 실행한다. (HTTP, API call, DB read 등...)
이벤트 루프가 이 페이즈에 도달하면, 이름 그대로 폴링을 수행하는데 주요 기능은 두가지이다.
- I/O를 얼마나 블록하고 폴링해야하는지 계산함.
- poll 큐에 존재하는 이벤트를 처리함.
만약, 더 이상 수행할 콜백들이 없을 경우 로직이 조금 복잡한데 다음 페이즈(check, close calbacks)에 해야할 작업이 있는지 확인 후 있다면 다음 페이즈로 넘어가고, 없다면 다음 페이즈로 넘어가지 않고 블록되게 된다.
블록 된 이후에도 마냥 기다리는 것이 아니라 타이머 힙에서 실행 가능한 작업이 있는지 확인하여 존재하면 그 타이머의 지연 시간만큼만 대기 후 timers 페이즈로 이동한다.
(타이머 작업이 있다고 무작정 timers 페이즈로 가지 않는 이유는, 만약 타이머를 실행할 수 있는 시간이 아닐 경우 timers 페이즈가 그냥 끝나버리게 되므로 그냥 poll 페이즈에서 대기하는게 유리하기 때문이다.)
5. check 페이즈
check 페이즈에서는 오직 setImmediate와 관련된 콜백들만 처리하게 된다.
다른 페이즈와 마찬가지로 큐가 비거나, 최대 실행 개수를 초과할 때까지 콜백들을 실행한다.
6. close callbacks 페이즈
마지막으로, close callbacks 페이즈에서는 일부 close이벤트와 관련된 콜백들을 처리한다.
(예를 들면, 소켓 연결을 닫을 때 사용하는 socket.on('close', ...)과 같은 것들이다.)
이 close callbacks 페이즈가 종료되고 나면 이벤트 루프는 다음 루프가 존재하는지 체크하고, 없다면 그대로 이벤트 루프는 종료된다.
만약, 더 수행해야할 작업들이 남아있다면 첫 페이즈인 timers 페이즈로 돌아가 다시 순회를 시작한다.
# nextTickQueue 및 microTaskQueue
앞서 설명한 콜백 함수들은 각각 taskQueue에 담겨 실행되게 되는데, 이 taskQueue이외에 다른 큐도 존재한다.
그것은 바로 nextTickQueue와 microTaskQueue인데, 이 두가지 큐는 libUV 라이브러리에 포함되어 있지 않으므로 이벤트 루프의 일부가 아니며, taskQueue에 비해 높은 우선순위를 가지고 있을 뿐 아니라 현재 실행중인 페이즈가 끝나 다음 페이즈로 넘어가기 전 즉시 실행된다.
(이 큐들은 콜백 최대 실행 한도의 제한이 없다.)
콜백 함수를 taskQueue에 넣는 작업은 다음과 같다.
- setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI 렌더링
콜백 함수를 microTaskQueue에 넣는 작업은 다음과 같다.
- process.nextTick, Promise, Object.observe, MutationObserver
이 중에서 process.nextTick 요녀석이 조금 특이한데, microTask 중에서도 살짝 더 우선순위가 높고 nextTickQueue라는 별도의 큐가 존재한다.
한 페이즈에서 다음 페이즈로 넘어가는 과정을 Tick이라고 하며,
큐들의 우선순위를 정리하면 nextTickQueue > microTaskQueue > taskQueue 순이다.
이렇게 이론을 공부한 후, 실제 코드를 통해 이것저것 실험을 해보았는데...
여기에 정리해 두었으니 참고하자!
'Language & Runtime > Node.js' 카테고리의 다른 글
[Node.js] 나의 첫 오픈소스 angxios 개발기 (1) | 2022.12.05 |
---|