✏️ Node.js ➡️ 네트워크 서버 구축에 특화되어 있고, V8엔진은 기존의 인터프리터 언어인 자바스크립트를 컴파일 방식으로 처리해 빠른 속도로 작업을 수행할 수 있도록 합니다. 또한, Livub 라이브러리르 사용함으로 비동기 I/O 처리가 가능하게 해 Blocking 없이 동시 요청이 가능하다.
이전에 node.js에 대해 공부를 하고 정리를 하여 글을 올렸다. Node.js를 공부하다보니 node.js에서 필수적으로 알아야 할 것들이 적어도 2가지가 있다고 생각했다. 첫 번째는 V8엔진, 두 번째는 libuv 라이브러리였다. V8엔진이 어떻게 자바스크립트 코드를 해석하고 실행되는지 또한 정리를 했었고 이번에는 libuv라이브러리가 무엇인지, Node.js안에서 어떤 역할을 담당하고 있는것인지 알아보려 한다.
📖 용어설명
I/O
: 인풋과 아웃풋이 있는 입출력 작업들을 말한다. 파일 시스템(읽기/쓰기), 네트워크(데이터 송/수신, DNS) 등이 있다.
Non-blocking(논블로킹)
: 어떤 서브루틴을 처리할 때 원래 프로그램의 흐름이 멈추지 않는 것이다. Node.js에서 논블로킹은 자바스크립트가 아닌 다른 작업 때문에 이벤트 루프가 멈추는 것을 의미한다
Node.js는 이벤트기반 싱글 스레드, Non-blocking I/O의 모델을 구현하고 있다. 하나의 스레드로 동작을 하는데 blocking이 없는 모델이라?? 만일 스레드 중간에 처리해야 할 파일의 크기가 큰 작업이 들어오게 되면 그 뒤의 작업들은 앞의 작업이 끝날 때까지 기다려야 하는 blocking이 발생하게 되는데 Node.js는 싱글스레드로 blocking이 없게끔 어떻게 작동할까?
그 비밀은 바로 Event Loop(이벤트 루프)이다✨
하나의 스레드, 즉 메인 스레드가 이벤트 루프에 의해 관리되기 때문에 싱글스레드이지만 막히지 않고 스무스~하게 진행될 수 있는 것이다.
Node.js는 시간이 많이 걸리는 I/O작업을 메인스레드가 아닌 비동기 I/O를 지원하는 libuv에 위임하는데 그 기반에 이벤트 루프가 있다.
🔁 이벤트 루프
이벤트 루프는 libuv 라이브러리 내에 있는 장치로, Node.js가 작업들을 커널이나 스레드풀로 넘겨서 비동기 Non-blocking I/O작업을 할 수 있게 해주는 핵심 요소이다.
위의 그림처럼 1차선 도로에서 경찰이 마약수사를 위해 수하물검사를 하고 있다고 생각해 보자. “이벤트 루프”는 그 도로를 주시하고 있다가 트럭과 같이 짐이 많아 소요시간이 오래 걸릴 것과 같은 차량을 따로 빼서 검사를 하고 이상이 없으면 다시 1차선 도로(메인스레드)에 합류시켜 검사가 block 되지 않게 하는 느낌이라고 이해했다…😂(이벤트루프의 자세한 내용은 동기/비동기를 다루며 따로 정리를 해보려 한다.)
🛠 Libuv
✏️ libuv is a multi-platform support library with a focus on asynchronous I/O
- 비동기 입출력, 이벤트 기반에 초점을 맞춘 라이브러리.
- 비동기, 논블로킹 스타일 사용
libuv는 커널단(윈도의 경우 IOCP, 리눅스는 AIO)에서 어떤 비동기 작업들을 지원해 주는지 알고 있기 때문에(실제 I/O작업은 Kernel Level(OS)에서 일어나는 과정이다), 해당 종류의 비동기 작업들을 받으면, 커널의 비동기함수들을 호출한다. 작업이 완료되면 시스템콜을 libuv에게 던저주면(notify) libuv 내에 있는 이벤트루프에게 콜백으로서 등록된다.
libuv 라이브러리 내부적으로 존재하는 스레드풀(thread pool)에서는 커널에서 지원하지 않는 작업이나 이벤트 루프를 블로킹할 수 있는 시간이 오래 걸리는 몇몇 작업들을 수행한다.쓰레드 풀은 I/O 작업을 그 안에 존재하는 thread로 처리하기에 event loop에 시간이 오래걸리는 작업이 들어와도 block당하지 않고 빠르게 작업을 계속 진행할 수 있다. 또한 쓰레드 풀은 기본적으로 4개의 쓰레드로 구성되어 있고 C++ 코드로 해당 작업을 실행하여 블로킹 I/O와 CPU-intensive 작업을 포함하는 비동기 요청을 완료한다.
일반적인 js코드들은 싱글스레드인 이벤트루프에 의해 작동되지만 libuv라이브러리의 쓰레드 풀은 멀티스레드로 돌아가기에 “Node.js는 싱글스레드다!”라는 말은 잘못된 말이다.
Node.js는 싱글스레드라는 생각 때문에 CPU 작업량이 많은 프로그램 같은 경우 node.js와 어울리지 않고 적합하지 않다고들 이야기하는 것 같다. 하지만 Node.js는 싱글스레드가 아니라는 점과 버전 10.5부터 thread pool에 스레드를 프로그래머가 worker_thread라는 모듈을 통해 스레드를 생성할 수 있다는 점으로 인해 웹뿐만 아니라 점점 더 많은 분야에서 node.js가 사용되는 게 아닐까 싶다.
libuv는 커널의 비동기함수나 스레드 풀에게 작업을 비동기 형태로 전달 후 콜백을 큐에 저장한다. 큐는 성격에 따라 유형에 맞는 큐에 등록이 되고 그리고 이벤트루프가 돌면서 각 큐에 들어있는 콜백을 가져와 실행을 시킨다.(
사실 비동기로 던져놓은 상황에서 기존 코드들이 쭈욱 실행이 될 테고 이 코드들이 다 실행이 되면 이벤트루프를 만들지 종료할지는 libuv에 의해 콜백이 등록되었는지의 여부에 따라 달라진다. 등록이 되었다면 그 콜백을 실행시키기 위해 이벤트 루프가 돌면서 순서에 따라 큐에 등록된 작업을 실행하게 된다.
Node.js의 이벤트 루프는
- Timer Phase
- Pending Callbacks Phase
- Idle, Prepare Phase
- Poll Phase
- Check Phase
- Close Callbacks Phase
총 6개의 phase로 구성되어 있으며 위에서부터 차례대로 Timer Phase -> Pending Callbacks Phase -> Idle, Prepare Phase -> Poll Phase -> Check Phase -> Close Callbacks Phase -> Timer Phase 순을 따른다.(한 페이즈에서 다음 페이즈로 넘어가는 것을 틱(Tick)이라고 부른다)
각 페이즈는 자신만의 큐를 하나씩 가지고 있는데, 이 큐에는 이벤트 루프가 실행해야 하는 작업들이 순서대로 담겨있다. Node.js가 페이즈에 진입을 하면 이 큐에서 자바스크립트 코드(예를 들면 콜백)를 꺼내서 하나씩 실행한다. 만약 큐에 있는 작업들을 다 실행하거나, 시스템의 실행 한도에 다다르면 Node.js는 다음 페이즈로 넘어간다.
(각 phase의 대한 자세한 내용과 설명은 이곳에서 보다 자세히 확인할 수 있다.)
📌 한 줄 요약
➡️ Node.js는 I/O요청이나 시간소모가 많은 작업이 들어오면 백그라운드에서 실행시켜 놓고 다른 작업을 하다가 스레드풀과 커널이 제공하는 api로 처리하여 작업을 콜백으로 넘기면 이것들을 이벤트 루프가 돌리면서 실행이 되는 구조로 설계되었기 때문에 싱글스레드이지만 좋은 성능을 낼 수 있는 것이다
마치며
libuv가 무엇인지 검색을 하고 여러 블로그와 설명을 읽어도 읽을 때마다 다르고 작성한 사람마다 조금씩 설명이 달라서 이해하기가 조금 어려웠던 것 같다. 그래서 나의 말로 정리를 꼭 해야겠다는 생각이 들어서 정리해 보았다. 분명 나의 글 또한 다른 사람이 읽었을 때 이해가 되지 못하는 부분과 부족한 부분이 있을 것 같다. 계속해서 부족한 부분은 채우고 잘못된 부분은 지우며 결과적으로 내가 사용하는 Node.js에 대한 더 깊은 이해에 다다를 수 있기를 바란다.
참조
https://nodejs.org/ko/docs/guides/dont-block-the-event-loop
https://www.korecmblog.com/node-js-event-loop/
https://evan-moon.github.io/2019/08/01/nodejs-event-loop-workflow/
'TIL' 카테고리의 다른 글
Promise(프로미스) (0) | 2023.03.28 |
---|---|
동기 / 비동기 (0) | 2023.03.27 |
V8엔진 구조 및 작동 방법 - 2 (0) | 2023.03.23 |
나는 왜 Node.js를 사용했을까? - 1 (0) | 2023.03.15 |
Process & Thread (0) | 2023.03.13 |