-
Node.js 이벤트 루프카테고리 없음 2022. 3. 16. 23:54
Node.js?
대부분의 노드 책을 펴자마자 알 수 있는 것은 Node.js는 V8 엔진 기반의 event-driven, 논블로킹, 싱글스레드 js 런타임 이다. 라고 말을 한다. Spring Boot 혹은 Spring MVC 같은 경우에는 멀티스레드 기반이다. 새로운 request가 올때마다 새로운 스레드를 하나씩 polling 한다. 하지만 Node 같은 경우에는 하나의 스레드 만으로 여러 비동기 작업을 블로킹 없이 수행할 수 있고 그 기반은 event-loop 가 존재 한다.
위 사진은 Node.js 의 구성도 인데 Node.js 는 C++로 작성된 런타임이고 그 내부에 V8 엔진과 libuv를 가지고 있다.
libuv?
libuv는 C++로 작성되었고, Node.js 가 사용하는 비동기 I/O 라이브러리다.
크게 다음과 같은 역할을 한다.
- 네트워크, 파일 I/O등 비동기 처리지원
- 비동기는 시스템마다 제공하는 API 사용
- 네트워크, 소켓 작업은 시스템 API를 사용하며, 파일을 스레드 풀을 사용
const fs = require('fs') for(let i=0; i<10; i++) { fs.readFile('./test.txt',(err,data) => { console.log(data.toString()); }) } console.log('end')
다음과 같은 코드가 있다고 가정을 했을 때 fs.readFile 함수를 호출하면 파일을 읽기 위하여 libuv가 호출이 된다. node가 파일을 읽기 위하여 fs.readFile을 호출하면 내부적으로 c++로 바인딩 되어있는 자바스크립트 코드를 이용하여 libuv를 호출 한다.
fs.readFile에 전달된 콜백은 이벤트 루프에 진입을 해야 실행을 한다. 코드의 마지막줄 console.log('end')를 실행하면 node.js는 이벤트 루프를 실행 할 지 검사한다.
앞에서 fs.readFile을 실행했기에 libuv에 있는 uv_run을 호출해서 이벤트 루프를 진입해야 한다.
쉽게 풀어 설명을 하면 Node.js는 I/O 작업을 libuv에 위임함으로서 논 블로킹 I/O를 지원하고 그 기반엔 이벤트 루프가 있다.
이벤트 루프
아래의 사진은 nodejs 공식문서에서 제공하는 이벤트 루프의 구조이다.
또한 추가적으로 nextTickQueue와 microTaskQueue가 있는데 이는 이벤트 루프의 일부가 아니다.
하지만 Node.js의 비동기 작업 관리를 도와주는 것들로 아래에서 다뤄 보겠다.
각각의 단위는 페이즈(phase)로 구분이 되며, 각각의 페이즈가 넘어갈때 넘어가는 단위는 틱이 된다.
각 페이즈는 자신만의 큐를 하나씩 가지고 있다. 그리고 이 큐에는 이벤트 루프가 실행해야 하는 작업들이 순서대로 담겨있다. 만약 큐에 있는 작업들을 다 실행하거나, 시스템의 실행 한도에 다다르면 Node.js는 다음 페이즈로 넘어간다.
순서는 그림에서와 같이
Timers Phase => Pending Callbacks Phase => Idle, Prepare Phase => Poll Phase => Check Phase => Close Callbacks Phase
이렇게 구성이 된다.
Timers Phase
이벤트 루프의 가장 앞에 위치한 페이즈이자 시작을 알리는 페이즈 이기도 하다. setTime이나 setInterval 같은 타이머들의 콜백을 저장한다. 이 페이즈에서 바로 타이머들의 콜백이 큐에 들어가는 것은 아니다. 타이머들이 min-heap으로 느슨하게 관리되고 있다는 것이다.
Pending Callbacks Phase
이 페이즈는 이벤트 루프의 pending_queue에 들어있는 콜백들을 실행한다. 이 큐에 들어와있는 콜백들은 이전 이벤트 루프에 수행되지 못한 I/O 콜백들이다. 혹은 에러 핸들러 콜백들을 실행한다.
Idle, Prepare Phase
이 페이즈는 공식문서에도 다른 설명이 벗고 코드의 실행에 직접적인 영향을 미치지 않아 생략 하겠다.
Poll Phase
이 페이즈는 파일 시스템 I/O, 네트워크 I/O를 마치고 실행될 콜백들을 처리하는 단계이다. 다시 말해 새로운 I/O 이벤트를 다루며 watcher_queue의 콜백들을 실행한다.
setTimeout, setImmediate, close 콜백등을 제외한 거의 모든 콜백이 여기서 처리가 된다. (시스템 제한 범위 내에서)
- HTTP요청을 보냄
- 쿼리수행
- 파일 다 읽었을 때
watcher_queue에 I/O 작업이 차 있는 상태면 콜백을 하나씩 처리를 하게 되지만 큐가 비어 있으면 다음중 하나의 작업을 수행 한다.
- setImmediadte() 함수로 스케쥴링된 콜백이 존재하는 경우엔, poll phase를 종료 하고 이를 처리하는 check 단계로 넘어간다.
- check 단계에서 처리할 콜백이 없는 경우에는 timer 큐를 확인한다. 만약 timer 큐가 차있으면 해당 콜백이 실행될 수 있는 시간 만큼을 대기하고 다음단계로 넘어간다. 그렇지 않고 timer 큐 마저 비어있다면 새로운 이벤트를 수신할 떄 까지 poll phase에서 대기하게 된다.
쉽게 설명하자면,
더 이상 콜백들을 실행할 수 없는 상태가 된다면 check_queue, pending_queue, closing_callbacks_queue에 해야할 작업이 있는지를 검사하고, 만약 해야할 작업이 있다면 바로 Poll phase가 종료되고 다음 페이즈로 넘어가게 된다. 하지만 특별히 해야할 작업이 더 이상 없는 경우 Poll phase는 다음 페이즈로 넘어가지 않고 계속 대기하게 된다.
Check Phase
이 페이즈는 setImmediate()를 처리하기 위한 페이즈이다. setImmediate()가 호출 되면 Check Phase의 큐에 담기고 노드가 Check Phase에 진입하면 차례로 실행이 된다.
Close Callbacks Phase
소켓과 같은 close 이벤트 타입의 핸들러를 처리하는 페이즈다. timer 단계에서 수행할 콜백이 있는경우에는 이벤트 루프를 다시 순회한다. 그렇지 않은 경우, 이벤트 루프는 종료 된다.
nextTickQueue
위에서 말했듯이 nextTickQueue는 이벤트 루프에 속하지는 않는다. 이벤트 루프에 진입 하거나, 매 단계가 끝날 때 마다 처리되는 큐이다.
microTaskQueue
microTaskQueue는 nextTickQueue와 유사하지만, Promise의 콜백을 처리한다. 그리고 nextTickQueue가 microTaskQueue보다 우선 순위가 높다.
그렇다면 nextTickQueue와 microTaskQueue는 언제 처리가 실행이 되냐 이 두개 큐의 콜백들은 페이즈가 넘어가기 전에 자신이 가지고 있는 콜백들을 최대한 빨리 실행해야하는 역할을 맡고 있다.