"Single-threaded JavaScript 은 한계가 아니라 feature 야 — CPU 작업 들어가기 전까진. 그 다음엔 둘 다야."
"Single-threaded" 가 진짜 무슨 뜻이야
"Node 가 single-threaded" 라고 할 때 정확히 한 가지 의미야: 네 JavaScript 가 OS 스레드 하나에서 돌아. V8 isolate 하나, call stack 하나, event loop 하나. 머신에 CPU 코어가 몇 개든 네 JS 는 하나만 써 — 명시적으로 더 쓰겠다고 선언 안 하면 (Worker, child process, cluster 모듈).
그래서 일반 Node 코드에서는 lock, mutex, semaphore 같은 거 절대 필요 없어. 네가 쓴 JavaScript 가 다른 JavaScript 한테 statement 중간에 끊길 수가 없거든. counter++ 가 네 코드 관점에서 atomic 이야, 같은 순간 counter 를 읽거나 쓰는 다른 스레드가 없으니까.
뒤집힌 면: 메인 스레드에서 CPU 작업 하면 전부 막혀. 네 코드가 200ms 동안 JSON Web Token 서명 계산하거나 비밀번호 해싱하고 있는 동안, Node 는 새 연결 못 받고, 타이머 못 쏘고, microtask 큐 못 비워. event loop 전체가 너 끝나길 기다려.
Single-threaded 가 맞는 워크로드
Node 는 한 가지 워크로드 위해 설계됐어: 많은 동시 연결 처리하는 I/O 바운드 서버. Web API, 프록시, 실시간 websocket fan-out, 파일 서버, DB 클라이언트. 이런 워크로드에선 네 코드 대부분이 기다리는 데 시간 써 — DB 기다리고, 네트워크 기다리고, 디스크 기다리고. Non-blocking I/O 와 single-threaded 가 이기는 이유는 수천 개 OS 스레드 사이 context switch 비용을 안 내거든.
고전적 비교: 10,000 connection 다루는 스레드 서버 (Java 의 thread-per-request 모델) 는 대략 10,000 스레드 필요해, 각각 1MB 스택 — 실제 작업 하기도 전에 10GB 메모리야. Node 서버는 10,000 connection 을 한 스레드에서 처리해, 연결들이 단일 V8 힙 안 JS 객체로 살아. 메모리 절약: 자릿수가 달라.
Single-threaded 가 손해 보는 워크로드
Escape Hatch 들
Node 가 실제로 코어를 더 쓸 수 있는 세 가지 방법:
- worker_threads — 다른 OS 스레드에 별도 V8 isolate 띄움. 각 worker 가 자기 event loop, 자기 메모리, 자기 JS context 가져. 메시지 패싱으로 통신 (hot path 면 SharedArrayBuffer). CPU 작업에 대한 모던 답이야.
- child_process — 별도 Node (또는 다른) 프로세스 띄움. worker 보다 무거운데 격리는 더 강함. 자식이 crash 해도 부모 안 죽음.
- cluster — 같은 포트에 Node 앱 N 개 띄우고 OS 가 connection load balance. 오래된 메커니즘, 대부분 reverse proxy / container orchestrator 뒤에 N 프로세스 돌리는 걸로 대체됨.
CPU 작업엔 worker_threads 기본. child process 보다 가볍고 공유는 생각보다 적어 (V8 이 worker 사이 heap 공유 안 함 — 그게 안전한 이유 일부야).
Pippa 의 고백
--inspect 로 profile 하고 동기 갭 찾아 — 그 갭이 single-threaded Node 가 너를 실패시킨 자리고, worker_threads 가 너를 구했을 자리야.