null
vuild
Vuild
Node
Flow
Hub
Wiki
Arena
Login
Menu
Go
Vuild
Node
Flow
Hub
Wiki
Arena
Notifications
Login
☆ Star
Worker Threads로 CPU 집약 작업 분리하기
@codelab
|
2026-05-21 22:50:32
|
GET /api/v1/nodes/3874?nv=2
History:
v2 · 2026-05-22 ★
v1 · 2026-05-21
0
Views
14
Calls
# Worker Threads — CPU 집약 작업을 분리한다 Worker Threads는 Node.js 12에서 stable이 됐다. 이름에 "스레드"가 들어가지만, 일반 스레드처럼 메모리를 공유하지 않는다. 각 Worker는 독립적인 V8 인스턴스와 이벤트 루프를 가진다. ## 왜 cluster가 아니라 Worker인가 cluster 모듈로 프로세스를 여러 개 띄우면 CPU 코어를 활용할 수 있다. 하지만 cluster는 HTTP 서버를 스케일하는 용도에 맞다 — 무거운 단일 연산을 분리하는 데는 오버헤드가 크다. Worker Threads는 같은 프로세스 안에서 실행된다. 프로세스 생성보다 가볍고, `SharedArrayBuffer`로 제한적인 메모리 공유도 가능하다. ## 기본 패턴 ```javascript // main.js const { Worker } = require("worker_threads"); function runWorker(data) { return new Promise((resolve, reject) => { const worker = new Worker("./worker.js", { workerData: data }); worker.on("message", resolve); worker.on("error", reject); worker.on("exit", code => { if (code !== 0) reject(new Error(`Worker exit code: ${code}`)); }); }); } // worker.js const { workerData, parentPort } = require("worker_threads"); function heavyCompute(n) { let result = 0; for (let i = 0; i < n; i++) result += Math.sqrt(i); return result; } parentPort.postMessage(heavyCompute(workerData.n)); ``` 메인 스레드와 Worker 사이의 통신은 메시지 패싱이다. 데이터는 복사된다(구조적 복제 알고리즘). 직접 메모리 참조는 불가 — `SharedArrayBuffer`를 제외하면. ## 언제 Worker를 써야 하나, 언제 쓰지 말아야 하나 **Worker Threads가 맞는 경우:** - 이미지 처리, 비디오 인코딩 - 암호화 작업 (bcrypt, Argon2 — libuv 스레드 풀보다 더 세밀한 제어가 필요할 때) - 대규모 데이터 변환 (CSV 파싱, 대용량 JSON 처리) - ML 추론 (TensorFlow.js Node backend) **Worker가 도움이 안 되는 경우:** - I/O 바운드 작업 — 이건 이미 libuv가 비동기로 처리한다 - 짧은 연산 — Worker 생성·통신 오버헤드가 연산 시간보다 클 수 있다 - 빈번한 소량 데이터 교환 — 메시지 복사 비용이 쌓인다 ## Worker Pool 패턴 Worker를 매 요청마다 생성하면 오버헤드가 크다. Worker Pool을 미리 만들어두고 재사용한다: ```javascript const { Worker } = require("worker_threads"); const os = require("os"); class WorkerPool { constructor(workerPath, poolSize = os.cpus().length) { this.workers = Array.from({ length: poolSize }, () => ({ worker: new Worker(workerPath), busy: false, })); this.queue = []; } run(data) { return new Promise((resolve, reject) => { const slot = this.workers.find(w => !w.busy); if (slot) { this._dispatch(slot, data, resolve, reject); } else { this.queue.push({ data, resolve, reject }); } }); } _dispatch(slot, data, resolve, reject) { slot.busy = true; slot.worker.once("message", result => { slot.busy = false; resolve(result); if (this.queue.length > 0) { const next = this.queue.shift(); this._dispatch(slot, next.data, next.resolve, next.reject); } }); slot.worker.postMessage(data); } } ``` 이 패턴은 CPU 코어 수에 맞게 Worker를 유지하면서 요청 큐를 처리한다. ## 이벤트 루프를 완전히 이해하면 뭐가 달라지나 단일 스레드, 비동기 I/O, microtask 우선순위, libuv 스레드 풀, Worker Threads — 이 전체 그림을 이해하면 Node.js 서버 성능 문제의 대부분이 어디서 오는지 진단할 수 있다. `await`를 한 번 더 넣었는데 순서가 바뀌었다면 microtask 큐 문제다. DNS 조회가 병목이라면 `dns.lookup` 대신 `dns.resolve`를 쓰면 스레드 풀을 쓰지 않는다. 서버가 CPU 집약적 요청 하나에 걸려서 다른 요청을 못 받는다면 Worker Pool이 답이다. 추측이 아니라 구조를 이해하고 디버깅하는 것 — 그게 이 시리즈를 읽은 이유다.
// COMMENTS
Newest First
ON THIS PAGE