Node.js 멀티스레딩: Worker Threads로 CPU 집약 작업 처리하기
문제 상황
이미지 업로드 API에서 여러 사이즈의 썸네일을 생성하는 작업이 있었다. 한 번에 20~30장의 이미지를 처리하다보니 메인 스레드가 블로킹되어 다른 요청들이 지연되는 문제가 발생했다.
기존에는 Sharp 라이브러리를 사용해 동기적으로 처리했는데, 처리 시간이 5초를 넘어가면서 타임아웃이 발생하기 시작했다.
Worker Threads 도입
Node.js 12부터 안정화된 Worker Threads API를 활용하기로 했다. 메인 스레드는 요청 라우팅만 담당하고, 실제 이미지 처리는 워커 풀로 분산시키는 구조다.
const { Worker } = require('worker_threads');
const os = require('os');
class WorkerPool {
constructor(workerPath, poolSize = os.cpus().length) {
this.workerPath = workerPath;
this.poolSize = poolSize;
this.workers = [];
this.queue = [];
for (let i = 0; i < poolSize; i++) {
this.workers.push(this.createWorker());
}
}
createWorker() {
return {
worker: null,
busy: false
};
}
async exec(data) {
return new Promise((resolve, reject) => {
this.queue.push({ data, resolve, reject });
this.processQueue();
});
}
processQueue() {
const availableWorker = this.workers.find(w => !w.busy);
if (!availableWorker || this.queue.length === 0) return;
const { data, resolve, reject } = this.queue.shift();
availableWorker.busy = true;
const worker = new Worker(this.workerPath, { workerData: data });
availableWorker.worker = worker;
worker.on('message', (result) => {
resolve(result);
availableWorker.busy = false;
worker.terminate();
this.processQueue();
});
worker.on('error', (err) => {
reject(err);
availableWorker.busy = false;
worker.terminate();
this.processQueue();
});
}
}
module.exports = WorkerPool;
워커 스크립트는 별도 파일로 분리했다.
// imageWorker.js
const { parentPort, workerData } = require('worker_threads');
const sharp = require('sharp');
async function resizeImage() {
const { inputPath, outputPath, width, height } = workerData;
await sharp(inputPath)
.resize(width, height, { fit: 'cover' })
.toFile(outputPath);
parentPort.postMessage({ success: true, outputPath });
}
resizeImage().catch(err => {
parentPort.postMessage({ success: false, error: err.message });
});
결과
4코어 서버 기준으로 처리 시간이 약 60% 단축되었다. 더 중요한 건 메인 스레드가 블로킹되지 않아 다른 API 응답 시간에 영향을 주지 않게 되었다는 점이다.
워커 풀 크기는 CPU 코어 수로 설정했는데, 테스트 결과 이게 가장 효율적이었다. 더 늘리면 컨텍스트 스위칭 오버헤드가 발생했다.
주의사항
Worker Threads는 CPU 집약 작업에만 유효하다. I/O 작업은 기본 비동기 모델이 더 효율적이다. 또한 워커 간 데이터 전송에 직렬화 비용이 발생하므로 대용량 데이터는 파일 경로만 전달하는 방식을 택했다.