예전 포스팅에서 Node.js의 이벤트 루프(event loop)의 개념과 동작 원리에 대해 자세히 알아보았다.
https://ilikezzi.tistory.com/68
[Javascript] 비동기의 핵심, 이벤트 루프(Event loop) 파헤치기
메리 크리스마스 🎄 크리스마스인데도 포스팅을 하는 이유는 백엔드파트 리더님과 대화중에 JS 이벤트루프를 설명해보라고 하셨는데 어버버대다가 제대로 설명을 못해서 당황했었다.. 리더님
ilikezzi.tistory.com
이번 포스팅에서는 그 이벤트 루프를 포함하고 있는 Node.js의 핵심 비동기 처리 라이브러리인 libuv를 중심으로,
NestJS를 사용하면서 반드시 이해해야 할 장단점과 부하에 대한 내구성 관리에 대해서 깊이 있게 다뤄보겠다.
많은 개발자들이 Node.js에 대해 "싱글 스레드라서 I/O에는 강하지만 CPU 집약적 작업에는 약하다"라고 알고 있는데,
정확히 왜 그런지, 그리고 그 한계를 어떻게 극복할 수 있을지 하나씩 알아보자.
왜 CPU 집약 작업에 약할까?
먼저 Node.js의 단점부터 살펴보자.
Node.js는 기본적으로 싱글 스레드 + 이벤트 루프 구조로 동작한다.
Java/Spring처럼 멀티 스레드가 아닌 싱글 스레드에서 JavaScript를 실행하기 때문에,
CPU 집약적인 작업이 들어오면 이벤트 루프가 블로킹되어 버린다.
for (let i = 0; i < 1e9; i++) {
Math.sqrt(i);
}
예를 들어, 이런 코드가 있다면
이 연산은 무조건 메인 스레드에서 실행되고, 이벤트 루프를 막아버린다.
그 결과 API 응답이 지연되고 전체 애플리케이션이 멈춘 것처럼 동작하게 된다.
하지만 이 문제도 해결 방법이 있다.
Node.js에서 이러한 CPU 집약적 작업을 처리할 때는 크게 두 가지 방법으로 해결할 수 있다.
1. Worker Threads 활용
import { Worker } from 'worker_threads';
new Worker('./heavy-task.js'); // 별도의 쓰레드에서 연산
Worker Threads는 각각 독립적인 V8 엔진 인스턴스를 워커마다 띄워서,
순수 CPU 연산(암호화, 영상 처리, 대규모 계산 등)을 진정한 병렬로 수행할 수 있다.
이미지 처리(이미지 리사이징), 복잡한 수식 계산, 텍스트 파싱 등의 작업에 적합하다.
2. 클러스터 모드 (PM2) 활용
pm2 start app.js -i max
클러스터 모드는 멀티코어를 활용하여 CPU 코어 수만큼
Node.js 인스턴스를 띄워 병렬 처리를 가능하게 해준다.
이렇게 하면 멀티코어를 활용해서 부하를 분산시킬 수 있다.
이런 방법들을 통해 NestJS의 싱글 스레드 구조로 인한 블로킹 문제를 상당 부분 해소할 수 있다.
그럼 왜 I/O 작업은 장점일까?
이제 핵심 질문이다.
왜 Node.js는 I/O 작업에 강할까?
단순히 이벤트 루프가 있어서일까?
아니다.
그 비밀은 바로 libuv라는 라이브러리에 숨어 있다.
이벤트 루프는 사실 libuv 안에 포함된 개념이고, libuv가 실제 I/O를 처리하는 핵심 엔진이다.
이게 바로 싱글 스레드이면서도 논블로킹 구조를 가질 수 있는 핵심이다!
libuv란 무엇인가?

libuv는 Node.js의 비동기/논블로킹 처리를 가능하게 해주는 C 기반 라이브러리다. (C++ 아님)
Node.js는 내부적으로 이런 구조로 되어 있다:

이전 Node.js 버전에서는 libeio, libev를 나눠서 사용했는데,
현재 우리가 사용하는 Node.js에서는 통합된 libuv로 변경되었다.
| 컴포넌트 | 언어 | 역할 |
| V8 | C++ | 자바스크립트 실행 엔진 |
| libuv | C | 비동기 I/O 처리 (이벤트 루프, Thread Pool 포함) |
| Node.js 바인딩 코드 | C++ | JS → C/C++ 연결 코드 (fs, crypto 등) |
libuv의 특징에 대해 알아보자.
주요 특징:
- 네트워크 요청, 파일 읽기, 타이머, DNS, 비동기 I/O 전부 libuv가 처리
- OS에 작업을 위임하고 결과를 기다림
- 시스템마다 제공하는 최적화된 API를 활용
libuv가 정말 똑똑한 점은 각 운영체제가 제공하는 최적화된 API를 활용한다는 것이다:
- Windows: IOCP (I/O Completion Ports)
- Linux: epoll
- macOS: kqueue
참고로 Spring의 경우 Linux 환경에서 select를 사용하는데, libuv는 더 효율적인 epoll을 사용한다.
libuv의 내부 동작 원리

Node.js 내부적으로 비동기 작업이 발생하면 다음과 같은 흐름으로 처리된다.
- JavaScript 코드 실행: 스택에 코드가 쌓임
- libuv 호출: 스택에 쌓인 코드를 실행하면서 libuv를 호출
- 작업 분류: libuv가 비동기 처리할지, 동기 처리할지 검사
- 작업 위임: (비동기 작업) 시스템 API를 이용하거나 Thread Pool에게 작업 위임
- 시스템 API: 소켓 네트워크, 파이프, 파일 디스크립터 감시, 타이머 등
- ThreadPool: 파일 시스템 read/write, DNS getaddrinfo, xrypto, zlib 등 - 콜백 등록: 작업이 완료되면 콜백 함수를 태스크 큐에 전달
- 이벤트 루프: 콜스택이 비었을 때 태스크 큐의 콜백을 콜스택으로 이동
- 콜백 실행: 콜스택에서 콜백 함수 실행 후 제거
즉, libuv가 내부적으로 작업을 처리하여 JS 실행 스레드는 블로킹되지 않고 다음 작업을 계속 수행할 수 있다.
실제 Node.js 공식 소스코드를 보면 이런 식으로 되어 있다:
int NodeMainInstance::Run() {
// ... 초기화 코드 ...
do {
uv_run(env->event_loop(), UV_RUN_DEFAULT);
per_process::v8_platform.DrainVMTasks(isolate_);
more = uv_loop_alive(env->event_loop());
if (more && !env->is_stopping()) continue;
// ... 기타 처리 ...
} while (more == true && !env->is_stopping());
// ... 정리 코드 ...
}
여기서 uv_run()이 이벤트 루프를 실행하는 핵심 함수다.
Node.js 인스턴스가 생성될 때 이 함수가 do-while문으로 계속 호출되면서 이벤트 루프가 동작한다.
중요한 점은 Node.js에서 동작하는 이벤트 루프는 libuv의 구현체라는 것이다.
libuv는 JavaScript 엔진이 아니기 때문에,
파라미터로 넘겨받은 v8::Isolate, v8::Context를 이용해서 JavaScript 로직을 처리한다.
Thread Pool의 이해
Node.js는 싱글 스레드라고 하지만, 이는 JavaScript를 실행하는 스레드가 하나라는 의미다.
실제로는 libuv 내부에 Thread Pool이 존재한다.
기본적으로 4개의 워커 스레드를 사용하며, 이렇게 조절할 수 있다:
process.env.UV_THREADPOOL_SIZE = 10
Thread Pool은 라운드 로빈이 아니라 단순 FIFO 큐 기반으로 실행된다.
예를 들어 10개의 요청이 들어오면:
- 처음 4개는 각 스레드에서 즉시 처리
- 나머지 6개는 대기 큐에서 순서대로 대기
- 스레드가 완료되는 순서대로 대기 중인 작업을 처리
즉, 동시에 기본 4개까지만 처리되고, 나머지는 풀 안에서 FIFO 순서로 대기하는 구조다.
libuv Thread Pool은 일부 CPU 작업도 처리할 수 있다.
처리 가능한 작업:
- crypto.pbkdf2와 같은 내장 비동기 API
- 파일 시스템 I/O(fs), zlib 압축, DNS 조회 등
처리 불가능한 작업:
- (메인 스레드에서 처리됨): 순수 JavaScript로 구현한 반복문이나 수학 연산
- 이미지 처리, 머신러닝과 같은 복잡한 연산
결론적으로 NestJS에서 성능을 최적화하기 위한 전략은 다음과 같이 정리할 수 있다.
- 순수 CPU 연산이 필요한 경우: Worker Threads 또는 PM2 클러스터 모드를 사용
- I/O 기반 작업(fs, crypto 등)인 경우: UV_THREADPOOL_SIZE를 늘려서 효율적으로 처리
이 두 가지 전략을 활용하면 Node.js의 단점인 CPU 집약적 작업에 대한 블로킹 문제를 효과적으로 해결할 수 있다.
아래 링크는 이벤트 루프와 libuv에 대해 더 깊이 이해하는 데 큰 도움을 받았어서 강추한다!
What you should know to really understand the Node.js Event Loop
Node.js is an event-based platform. This means that everything that happens in Node is the reaction to an event. A transaction passing…
medium.com
Node.js와 NestJS의 비동기 처리 메커니즘을 이해하는 것은 단순히 이론적 지식이 아니다.
실제 애플리케이션의 성능을 최적화하고, 병목 지점을 파악하며,
적절한 아키텍처를 설계하는 데 필수적인 지식이다.
참고:
https://bravochos.medium.com/the-internals-of-node-js-b57bab6dc90
https://github.com/nodejs/node
https://nodejs.org/ko/docs/guides/event-loop-timers-and-nexttick/
https://blog.naver.com/pjt3591oo/221976414901
https://sjh836.tistory.com/149
'Web > NestJS' 카테고리의 다른 글
| [NestJS] Winston 기반 로깅 라이브러리 'blanc-logger' npm 배포하기 (0) | 2025.03.05 |
|---|---|
| [NestJS] @automock/jest로 번거로운 Unit Test Mocking 자동화하기 (0) | 2025.02.02 |
| [Nest.JS] 구글, 네이버, 카카오 소셜로그인 구현 - 2 (1) | 2023.11.04 |
| [Nest.JS] 구글, 네이버, 카카오 소셜로그인 구현 - 1 (1) | 2023.10.16 |
| [Nest.JS] Nest.JS에서 cache-manager를 활용한 캐싱 방법 (0) | 2023.09.17 |