Node.js 의 동작 방식(프로세스, 스레드 풀, 이벤트 루프)
Node.js 를 통한 개발도 학습하기 위해, 오랜 시간 투자해 강의를 수강하고 있다. 이번 주제는 Node.js 의 동작 방식을 다뤄보도록 하자. 웹 브라우저와는 차이가 있으니, 주의해야한다.
libuv
이 주제를 다루기 위해서는, libuv 라는 것에 대해 알아야 한다.
libuv 는 비동기 I/O 에 초점을 맞춘 C언어 라이브러리로, 후술할 스레드 풀 / 이벤트 루프가 libuv 의 구조이기 때문이다.
Node.js 의 특징을 검색해보면, 가장 먼저나오는 ‘비동기 / 논블로킹 I/O’ 가 바로 이 라이브러리에서 나왔다는 말이다.
갑자기 웬 C 라이브러리냐고 할 수 있는데, Node.js 는 V8 이라는 자바스크립트 엔진이 존재한다.
V8 은 브라우저가 아닌 외부에서도 자바스크립트 코드를 실행할 수 있게 만든 것으로, 이 엔진이 C++ 기반으로 작성되어 있다. 그리고 이 엔진이 libuv 라는 라이브러리를 사용하는 구조다.
물론 더 복잡한 구조로 이루어져 있지만, Node.js 의 핵심은 libuv 라고 할 수 있을 정도로 중요하니 일단 여기까지만 이해해보자. 아무튼 그 덕에 Node.js 를 통해 백엔드도 구성할 수 있게 된 것이다.
싱글 스레드 구조
Node.js 의 대표적인 특징이 ‘싱글 스레드’라는 것인데, 실행 흐름이 단 하나 뿐이기에 코드 실행이 길어지면 어쩔 수 없이 동작이 멈추게 된다.
이 단점을 극복하기 위해, libuv 를 이용해 좀 더 많은 스레드를 사용한다.
Node.js 프로세스의 동작 방식
방금 Node.js 는 싱글 스레드라고 말했다. 프로세스 당 하나의 스레드만 갖게 되는데, libuv 를 통해서 이를 해결한다고도 말했다.
그렇다면, 어떻게 동작하는지도 아래 그림을 통해 알아보자.
모든 코드를 libuv 가 실행하는 것은 아니고, 오래걸리는 작업(암/복호화, 압축 관련)이나 시스템과 소통해야하는 코드(파일시스템, 통신)들을 libuv 가 도맡아 실행하는 방식이라고 생각하면 되겠다.
그렇다면 이 코드들이 어떻게 처리되는지 까지는 알아야할테니, 이벤트 루프에 대해서도 알아보자.
이벤트 루프(브라우저 vs Node.js)
Node.js 만을 다루는 게시글이긴 하지만, 브라우저 환경의 이벤트 루프도 다뤄보도록 하자.
알고 있겠지만, 자바스크립트를 실행하는 방식은 2가지가 있다.
첫 번째는 브라우저를 통한 방식, 두 번째는 Node.js 같은 실행환경을 통해 실행하는 방식이다.
두 가지 모두 이벤트 루프를 가지고 있지만, Node 의 경우엔 libuv 에 의존한다.
브라우저 환경에서는 별도로 구현이 되어있다.
중요한 점이 있는데, Node 의 경우엔 파일 시스템에 접근해서 작업이 가능하지만 브라우저에서는 이러한 코드 실행이 불가능하다.
반대로, 브라우저 환경에선 DOM 이벤트들을 다룰 수 있지만 Node 에서는 이런 이벤트가 없다.
상호 100% 보완의 관계라기 보단, 서로 다른 작업을 위해 존재하는 구조다.
브라우저의 이벤트 루프 구조
수강했던 Udemy 강의를 참고했다.
브라우저의 경우, WEB APIs 가 존재해서 별도의 콜백함수 실행을 도와준다.
각 브라우저를 전부 뜯어보는 건 어려우니, 이 정도 이해하고 넘어가도록 하자.
Node 의 이벤트 루프 구조
Node 의 경우, 파일시스템에 접근하거나 네트워크 작업을 진행하는 등의 콜백함수들을 libuv 에서 별도의 스레드로 처리해주는 방식이다.
libuv 는 조금 더 자세히 알아보고 넘어가보도록 하자.
다양한 콜백함수를 다루게 되면, 실행 우선 순위를 알아둬야 좋은 코드를 작성할 수 있으니 말이다.
libuv 의 이벤트 루프 페이즈
앞서 말했듯, 비동기 작업에도 우선 순위가 존재하고 있다.
총 6개의 페이즈가 존재하지만, 4개만 살펴보도록 하자.
- Timer Callback Phase
- Polling Callback Phase
- Check Phase
- Close Callback Phase
이벤트 루프는 이 Phase 들을 순환하는 구조를 가지며, Phase 마다 Queue 를 가지고 있다.
Timer Callback Phase
setTimeout()/setInterval()로 예약한 콜백 함수 실행
Polling Callback Phase
- I/O 이벤트 처리(파일 시스템, 네트워크 요청)
Check Phase
setImmediate()로 예약한 콜백 실행
Close Callback Phase
- 네트워크 요청 종료 시 실행하는 콜백 실행
- ex.
socket.on('close')
이렇게 4개 페이즈가 다음과 같은 순서로 실행된다.
Timer Callback -> Polling Callback -> Check -> Close
위 과정을 반복하면서, 이벤트 루프는 Callback Queue 에 남은 작업이 없을 때 까지 Queue 를 확인하며 남은 함수를 실행한다. 모든 함수를 실행하게 되면 종료한다.
마이크로 태스크 큐
마무리를 하기 전, 하나의 내용만 더 다루고 마무리해보자. 앞서 이야기한 각 Phase 사이사이에 마이크로 태스크 큐가 존재한다.
이건 libuv 내부에 존재하는 것이 아니고, Node.js 자체에 존재한다. 그런데, libuv 에서 한 Phase 의 작업을 마무리하면 Node 에서 그 다음 순서로 이 마이크로태스크 큐에 존재하는 작업을 처리하는 것이다.
이 마이크로 태스크 큐에서는 2가지의 콜백함수를 처리한다.
PROCESS.NEXTTICK()의 콜백Promise.then()(Resolved 상태의 Promise) 의 콜백
여기서 짚고 넘어갈 것은, resolved 상태의 프로미스에 접근하는 시점이 이 때라는 것이다.
마무리
Node.js 의 큰 특징인 싱글 스레드 구조에서 어떻게 비동기 / 논 블로킹 IO 수행이 가능한지 간단하게 살펴보았다.
이 내용을 다루기 위해 다른 게시글을 함께 살펴보니, 정말 심도있게 다룬 분들이 많았다.
내가 알고있는 내용이 정말 하찮게 느껴질 수준이지만…. 나도 노력해서 차근차근 정복해보도록 하자..!


