Node.js 워커 스레드로 CPU 집약 작업 개선하기
문제 상황
사용자가 업로드한 CSV 파일(평균 50MB)을 파싱하고 데이터를 가공하는 API 엔드포인트가 있었다. 파일 처리 중에는 다른 요청이 대기 상태에 빠지는 현상이 발생했다.
기존 코드는 메인 스레드에서 동기적으로 처리했다.
app.post('/upload', async (req, res) => {
const data = await parseCSV(req.file.path); // 5~10초 소요
const processed = processData(data); // 3~5초 소요
await saveToDatabase(processed);
res.json({ success: true });
});
Worker Threads 적용
Node.js 12부터 안정화된 worker_threads 모듈을 활용했다.
worker.js
const { parentPort, workerData } = require('worker_threads');
const { parseCSV, processData } = require('./processor');
(async () => {
const data = await parseCSV(workerData.filePath);
const processed = processData(data);
parentPort.postMessage(processed);
})();
main.js
const { Worker } = require('worker_threads');
app.post('/upload', async (req, res) => {
const worker = new Worker('./worker.js', {
workerData: { filePath: req.file.path }
});
worker.on('message', async (processed) => {
await saveToDatabase(processed);
res.json({ success: true });
});
worker.on('error', (err) => {
res.status(500).json({ error: err.message });
});
});
결과
- 파일 처리 중에도 다른 API 요청이 정상 처리됨
- CPU 코어를 효율적으로 활용 (4코어 기준 동시 4개 처리 가능)
- 메모리는 워커당 약 50MB 추가 사용
주의사항
워커 생성 비용이 있어서 매 요청마다 워커를 새로 만들면 오버헤드가 크다. 워커 풀을 구성하거나, 장시간 실행되는 작업에만 선택적으로 사용하는 것이 좋다.
다음에는 piscina 같은 워커 풀 라이브러리를 도입해볼 예정이다.