이 주제는 4월에 쓰기 시작하여 5월까지 다뤄보다가 포스팅을 하지 않았다. 단순히 javascript니까 당연히 싱글 스레드 아닌가,,, 했었는데 모호한 영역이 있었고. 그 부분을 제대로 이해하지 못한 채 올린다는 게 스스로 내키지 않았다. 다시 붙잡아보면서 스스로 이 주제에 대한 충분한 이해를 할 수 있길 바란다. 이전에 스레드와 프로세스에 다룬 글이 있다. 스레드의 개념을 먼저 정확히 알고 싶다면 해당 글을 먼저 보면 좋을 듯 하다.
2024.04.04 - [Software Engineering] - Difference between Process and Thread (+ Program)
Node.js
Node.js는 크로스 플랫폼 오픈소스 자바스크립트 런타임 환경으로 Window, Linux, macOS 등을 지원한다. Node.js는 V8 자바스크립트 엔진으로 구동되며, 웹 브라우저 바깥에서 자바스크립트 코드를 실행할 수 있다. 웹 서버와 같이 확장성 있는 네트워크 프로그램 제작을 위해 고안되었다.
- Single Thread
- Non-Blocking 비동기 I/O
- 이벤트 기반
- 경량 프레임워크
- 풍부한 라이브러리
- 서버와 클라이언트에서 사용하는 언어가 같다(JavaScript)
Node.js Story
2009년 Ryan Dahl은 Flickr의 파일 업로드 진행 표시줄을 보았을 때 파일이 얼마나 업로드되었는지 알기 위해서는 서버에 쿼리를 전송해야 한다는 점을 보고 조금 더 쉬운 방법을 찾다가 Node.js를 고안해냈다. 비동기 방식의 서버기술을 제공해서 사용자가 매번 물어보지 않아도 실시간으로 데이터를 주고받을 수 있게 해주었다.
왜 JavaScript 였을까? 🤔
Ryan Dahl은 위에서 말했듯 웹 서버에서 파일을 읽거나 네트워크 요청을 처리하는 것과 같은 입출력(I/O) 작업을 효율적으로 처리하고 싶은 목적이 있었다. JS는 싱글 스레드더라도 이벤트 루프를 통해 비동기 처리 방식을 이용해서 한 번에 많은 작업을 동시에 처리할 수 있다고 생각한 것이다. Ruby나 Python의 경우 동기식 접근 방식을 사용하고 있었는데 그것은 파일을 읽는 동안 다른 작업은 멈춰야 한다는 말이기에 목적을 실현하기엔 더욱 어려운 작업을 요했다.
Node.js의 탄생 배경, 역사에 대해 알고 싶다면 아래 다큐멘터리로 좀더 자세히 알아보는 것을 추천한다.
Node.js는 결국 싱글스레드라는 말아닌가?
Node의 구성요소
Node.js의 소스 코드는 C++과 JavaScript, Python으로 이루어져 있다.
상위 레벨은 JavaScript로 되어있고 로우 레벨은 C, C++로 짜여있다. Python은 빌드와 테스트에만 사용된다.

Node.js는 각 계층이 각 하단에 있는 API를 사용하는 계층의 집합으로 설계되어 있다.
1️⃣ 사용자 코드(JavaScript)는 2️⃣ Node.js의 API를 사용하고,
2️⃣ Node.js의 API는 C++에 바인딩 되어 있는 소스이거나 직접 만든 3️⃣ C++ 애드온을 호출한다.
3️⃣ C++에서는 4️⃣ V8을 사용해 JavaScript를 해석(JIT 컴파일러) 및 최적화하고 어떤 코드냐에 따라 C/C++ 종속성이 있는 코드를 실행한다. 또한 DNS, HTTP 파서, OpenSSL, zlib 이외의 C/C++ 코드들은
5️⃣ libuv의 API를 사용해 해당 운영체제에 알맞는 API를 사용한다.
구성요소 | 설명 |
Node.js API | 자바스크립트 API |
Node.js 바인딩 | 자바스크립트 C/C++ 함수 호출 |
Node.js 표준 라이브러리 (C++) | 운영체제와 관련된 함수들. 타이머(setTimeout), 파일시스템(Filesystem), 네트워크 요청(HTTP) |
C/C++ 애드온 | Node.js에서 C/C++ 소스를 실행할 수 있게 하는 애드온 |
V8 (C++) | 오픈 소스 자바스크립트 엔진. 자바스크립트를 파싱, 인터프리터, 컴파일, 최적화에 사용 |
libuv (C++) | 비동기 I/O에 초점을 맞춘 멀티플램폼을 지원하는 라이브러리, 이벤트 루프, 스레드 풀 등을 사용 |
기타 C/C++ 컴포넌트 | c-ares(DNS), HTTP 파서, OpenSSL, zlib |
Node.js의 구성요소 중 특히 V8과 libuv가 중요하다. V8은 JS 코드를 실행하도록 해주고, libuv는 이벤트 루프 및 운영체제 계층 기능을 사용하도록 API를 제공한다.
(아래는 Node.js 공식 문서의 About을 번역하여 들고왔다.)
About Node.js
// 아래 Hello World 예제에서 많은 연결을 동시 처리할 수 있다.
// 각 연결마다 콜백이 실행되지만 할 일이 없으면 Node.js는 대기 상태가 된다.
const { createServer } = require('node:http');
const hostname = '127.0.0.1';
const port = 3000;
const server = createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello World');
});
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
비동기 Event-Driven JavaScript 런타임으로 설계된 Node.js는 확장 가능한 네트워크 애플리케이션을 구축하기 위해 만들어졌다.
이는 동시성 모델과 대조적이며, 이 모델에서는 OS 스레드가 사용된다. 스레드 기반 네트워킹은 상대적으로 비효율적이며 사용하기 매우 어렵다. 또한, Node.js 사용자는 락lock*이 없기 때문에 프로세스의 데드락Deadlock에 대해 걱정할 필요가 없다.
=> Node.js가 여러 작업을 동시에 처리할 때 서로 경쟁하는 상황에서 각 작업이 데이터나 자원에 접근하는 것을 서로 차단하지 않는다는 뜻.
LOCK
일반적인 멀티 스레드 환경에서는 여러 스레드가 동시에 같은 자원에 접근하려 할 때, 데이터의 일관성을 보장하기 위해 '락’이라는 기술을 사용한다. 이 ‘락’은 한 스레드가 자원을 사용하고 있을 때 다른 스레드가 그 자원을 사용하지 못하게 막는 역할을 한다.
이런 ‘락’은 데드락이라는 문제를 발생시키는데, 두 개 이상의 프로세스나 스레드가 서로의 작업 완료를 무한히 기다리게 되는 상태를 말한다.
Node.js의 거의 모든 함수는 직접적인 I/O를 수행하지 않으므로, Node.js 표준 라이브러리의 동기 메소드를 사용하여 I/O가 수행될 때를 제외하고는 프로세스가 차단되지 않는다.
=> 이는 Node.js에서 확장 가능한 시스템을 개발하는게 합리적인 이유 중 하나이다.
Node.js는 Ruby의 Event Machine과 Python의 Twisted와 같은 시스템의 설계에 영향을 받았는데 한 단계 더 발전시켰다. 다른 시스템에서는 항상 이벤트 루프를 시작하는 차단 호출이 있다. 일반적으로, 스크립트 시작 시 콜백을 통해 동작이 정의되고, 마지막엔 EventMachine::run() 같은 차단 호출을 통해 서버가 시작된다.
Node.js에서는 이벤트 루프를 시작하는 그러한 호출이 없다. Node.js는 입력 스크립트를 실행한 후 간단하게 이벤트 루프에 진입한다. 더 이상 수행할 콜백이 없을 때 Node.js는 이벤트 루프를 종료한다. 이는 브라우저 JavaScript와 비슷하다
=> 이벤트 루프의 동작은 사용자로부터 숨겨져 있다. 즉, 직접 호출하거나 제어할 필요가 없다.
Node.js는 HTTP 프로토콜을 특별히 중요하게 다루고, 그 기능을 최적화하기 위해 설계되었다. 이는 Node.js를 웹 라이브러리나 프레임워크의 기반으로 적합하게 만들어준다.
- 스트리밍 : 데이터를 작은 조각으로 나누어 점진적으로 전송하는 방식. Node.js에서 스트리밍을 통해 대용량 파일이나 데이터를 효율적으로 처리한다.
- 낮은 지연 시간 : 데이터 요청과 그에 대한 응답 사이의 시간을 최소화한다.
Node.js는 기본적으로 싱글 스레드로 동작하지만, 서버의 여러 코어를 활용할 수 없는 것은 아니다. child_process.fork() API를 사용하여 새로운 Node.js 인스턴스를 자식 프로세스로 생성할 수 있다.
더 나아가 Node.js는 클러스터 모듈을 제공하여 여러 Node.js 인스턴스(워커)가 같은 서버 포트를 공유하며 실행될 수 있게 한다. 여러 워커에 자동으로 분산시켜주어 각 워커가 서로 다른 코어에서 실행될 수 있도록 함으로써 로드 밸런싱과 자원 활용의 최적화를 가능하게 한다.
⇒ 나는 바로 이 부분에서 Node.js가 과연 싱글 스레드인지 멀티 스레드인지 모호해진 부분이라 생각한다. 🤔
Node.js의 특징을 찾아보면 비동기 처리가 가능한 동시성을 가진 환경이라는 말이 있다. 정확히 무슨 말일까?
병렬(thread)과 비동기(async)
우선 비동기처리는 병렬 처리를 하는 방식이 아니라 단지 CPU Time sharing을 통해 여러 일을 동시에 작업하는 것처럼 보이게 하는 것일 뿐이다.
동시성은 하나의 스레드 위에서 여러 작업이 CPU가 한번씩 빠르게 돌아가면서 동시에 수행되는 것처럼 보이는 것
병렬성은 실제 CPU에서 여러 스레드를 물리적으로 동시에 수행되는 것
I/O 처리가 동시에 수행되는 것을 보고 병렬처리라고 생각해선 안된다, thread의 Task와 I/O 처리를 동일한 스레드의 태스크로 생각하면 잘못된 것이다.
해당 프로세스 내부가 아닌, 외부에서 물리적 로컬 장비(현재 컴퓨터 장비 등)를 타고 동작이 이루어져야 하는 I/O 처리(디스크에 쓰인 파일을 읽는 다던가, 혹은 네트워크 통신을 통해 네트워크 요청을 한다던가 등)들은 Node.js가 돌아가는 프로세스 바깥의 커널(하드웨어와 소프트웨어의 인터페이스)을 통해 디스크라던가 네트워크 장치를 통해 처리된 후 응답이 온다.
이처럼 I/O 처리가 커널을 통해 컴퓨터 내에서 작업이 별도로 돌아가는 것이므로, I/O 처리에 대한 응답이 오기까지 Node.js가 비동기 처리 요청만 보내놓고 이후에 다른 일을 할 수 있는 것이지, Node.js가 돌아가는 스레드 환경이 태스크를 병렬로 동시에 수행할 수 있어서가 아니다.
따라서 동시성 원리를 가진 비동기 처리는 병렬 처리가 아니다.
그래서,, 노드는 싱글스레드인가? 멀티 스레드인가?
Node.js는 하나의 프로그램으로, 실행 즉시 하나의 독립된 프로세스가 되어 CPU로부터 리소스를 할당 받는다. 노드에서 자바스크립트를 실행하면 콜백을 통해 여러 개의 비동기 함수를 실행할 수 있으며 fs.readfile 또는 DNS read와 같은 작업들도 수행할 수 있게 된다. 그러나 이벤트 루프에 큰 부담을 주어 결과적으로 앱 성능을 줄일 수 있다는 것이다. 이를 해결하기 위해 Node.js는 libuv이라는 C library를 사용하기 시작한다. 그리고 해당 라이브러리는 멀티 스레딩을 지원한다.
⇒ 기본적으로 싱글 스레드 모델을 사용하지만 내부적으로 멀티스레딩 기능을 활용하는 복잡한 시스템이다? 🤔

Worker Thread
Node.js 애플리케이션에서 별도의 스레드를 생성하여 빠르게 병렬처리를 할 수 있는 기능이다. 10.5 버전부터 나왔다.
Node.js는 워커스레드를 이용할 때, 작업을 처리할 새로운 스레드 풀을 생성 후 스레드 별 비동기 처리가 가능하도록 지원해 주는 libuv 엔진이 세팅된다. 그리고 각 스레드에서 Javascript 엔진으로 코드를 실행시킨다.
그리고 Node.js 환경 안에서 하나의 이벤트 루프를 통해 각 스레드들끼리의 응답 처리를 Task Queue를 통해서 전달된다.
=> 이는 Node.js에서 기존에 돌아가는 하나의 싱글 스레드 파이프라인이, 필요 시마다 추가로 스레드 파이프라인이 생성되면서 일을 처리할 수 있음을 뜻한다.

스레드 풀 thread pool
멀티 스레드 모델의 경우 thread pool을 두고 요청을 처리할 때 스레드를 기반으로 처리한다. 앞서 정리한 것처럼 대부분의 작업은 콜스택을 통해 처리되며 큐를 이용해 비동기 작업을 처리하지만 I/O, 네트워크 등의 작업처럼 OS 넘겨주는 작업은 논블로킹 방식으로 동작한다. 하지만 OS에서 지원하지 않는 비동기 작업이나 특정I/O 작업은 libuv에서 처리하게 되며 이는 내부적으로 운영되는 스레드 풀을 이용해서 논블로킹을 유지한다.
libuv를 사용하는 때는 언제일까?
1. DNS 쿼리를 할 때
- Host 의 ip 를 읽어야 하는 상황에서는 메인 스레드인 이벤트 루프를 사용하지 않고 새로운 스레드를 만들어 작업을 수행한다.
- Fetch API 등을 활용할 때가 이에 해당한다.
2. File system에 접근할 때
- Async (비동기) file system 작업을 할 때 이벤트 루프에서 자체적으로 새로운 스레드로 해당 작업을 넘긴다.
- 주의할 점은, readfilesync 와 같은 동기 함수는 새로운 스레드가 아닌 이벤트 루프에서 실행된다는 사실이다.
3. Crypto, Zlip
- ****Crypto 를 활용한 암호화나 해싱 작업 또한 이벤트 루프가 아닌 스레드를 활용한다.
- Zlip: 코드 내에서 무언가를 압축할 때 역시 스레드를 활용한다.
정리하자면 Node.js는 개념적으로 싱글 스레드이다. 그러나 멀티 스레딩이 가능하다.
Node.js의 프로세스는 이벤트 루프 + 스레드 풀로 구성되어있다.
- 이벤트 루프 ⇒ 싱글 스레드
- Thread Pool (libuv 내부, 비동기 처리를 지원한다.)
https://haeunyah.tistory.com/81
https://akasai.space/node-js/about_node_js_4/
https://nodejs.org/docs/v20.11.1/api/worker_threads.html#worker-threads
https://ko.wikipedia.org/wiki/Node.js
https://yceffort.kr/2021/04/nodejs-multithreading-worker-threads
https://yonghyunlee.gitlab.io/node/nodejs-structure/
https://www.youtube.com/watch?v=M3BM9TB-8yA
https://wikidocs.net/223219
https://helloinyong.tistory.com/350
https://blog.devgenius.io/node-js-cluster-but-with-worker-threads-25a441cfa158
https://sjh836.tistory.com/149
https://blog.naver.com/pjt3591oo/221976414901
https://h3yon.github.io/nodejs-thread/
ChatGPT
'JavaScript > Node.js' 카테고리의 다른 글
JWT란? NestJS로 풀어보는 토큰 기반 인증과 인가 (0) | 2025.01.30 |
---|---|
데이터 타입 걱정을 덜어주는 Nest.js ValidationPipe의 3가지 옵션 코드로 들여다보기 (1) | 2024.12.22 |