Node.js 18 Worker Threads로 CPU 집약 작업 병렬 처리하기
문제 상황
이미지 업로드 API에서 sharp 라이브러리로 리사이징을 처리하고 있었다. 단일 요청은 200ms 정도로 빠르지만, 동시에 10개 이상 요청이 들어오면 5초 이상 걸리는 경우가 발생했다.
Node.js는 싱글 스레드 이벤트 루프 기반이라 CPU 집약적인 작업이 메인 스레드를 블로킹하면 다른 요청도 대기하게 된다.
Worker Threads 적용
Node.js 18에서 정식 지원하는 Worker Threads를 사용해 이미지 처리를 별도 스레드에서 실행하도록 변경했다.
// worker.js
const { parentPort, workerData } = require('worker_threads');
const sharp = require('sharp');
sharp(workerData.buffer)
.resize(workerData.width, workerData.height)
.toBuffer()
.then(buffer => {
parentPort.postMessage({ success: true, buffer });
})
.catch(error => {
parentPort.postMessage({ success: false, error: error.message });
});
// api.js
const { Worker } = require('worker_threads');
const path = require('path');
function resizeImage(buffer, width, height) {
return new Promise((resolve, reject) => {
const worker = new Worker(path.join(__dirname, 'worker.js'), {
workerData: { buffer, width, height }
});
worker.on('message', (result) => {
if (result.success) {
resolve(result.buffer);
} else {
reject(new Error(result.error));
}
worker.terminate();
});
worker.on('error', reject);
});
}
결과
동시 10개 요청 시 평균 응답 시간이 5초에서 500ms로 개선되었다. CPU 코어 수만큼 병렬 처리가 가능해졌다.
다만 Worker 생성 오버헤드가 있어서 Worker Pool 패턴을 고려 중이다. piscina 같은 라이브러리를 검토해볼 예정이다.
주의사항
- Worker 간 데이터 공유는 structuredClone으로 복사된다
- Buffer는 transferList를 사용하면 복사 없이 전송 가능
- Worker 생성 비용이 있으므로 짧은 작업엔 비효율적
- 메모리 사용량이 증가하므로 Worker 수 제한 필요