Node.js 이벤트 루프 블로킹으로 인한 API 응답 지연 해결
문제 상황
운영 중인 Node.js API 서버에서 특정 엔드포인트가 호출되면 다른 모든 요청의 응답 시간이 함께 늘어나는 현상이 발생했다. 문제의 엔드포인트는 대량의 데이터를 CSV로 변환하는 기능이었다.
원인 분석
app.get('/export/csv', (req, res) => {
const data = db.getAll(); // 10만 건
let csv = 'id,name,date\n';
for (let row of data) {
csv += `${row.id},${row.name},${row.date}\n`;
}
res.send(csv);
});
10만 건의 데이터를 동기적으로 처리하면서 이벤트 루프가 블로킹되었다. Node.js는 싱글 스레드이기 때문에 이 작업이 완료될 때까지 다른 요청을 처리할 수 없었다.
해결 방법
1. Stream 방식으로 변경
const { Transform } = require('stream');
app.get('/export/csv', async (req, res) => {
res.setHeader('Content-Type', 'text/csv');
const csvTransform = new Transform({
objectMode: true,
transform(row, encoding, callback) {
callback(null, `${row.id},${row.name},${row.date}\n`);
}
});
db.stream().pipe(csvTransform).pipe(res);
});
2. Worker Threads 활용 (Node 10.5+)
무거운 연산이 필요한 경우 Worker Threads를 사용하여 메인 스레드를 블로킹하지 않도록 했다.
const { Worker } = require('worker_threads');
app.get('/export/csv', (req, res) => {
const worker = new Worker('./csv-worker.js', {
workerData: { query: req.query }
});
worker.on('message', (csv) => res.send(csv));
worker.on('error', (err) => res.status(500).send(err));
});
결과
Stream 방식으로 변경 후 해당 엔드포인트 호출 시에도 다른 요청의 응답 시간이 영향을 받지 않게 되었다. 평균 응답 시간이 2초에서 50ms로 개선되었다.
교훈
Node.js에서 CPU intensive한 작업을 할 때는 반드시 이벤트 루프 블로킹을 고려해야 한다. Stream, Worker Threads, 또는 작업 큐를 활용하여 메인 스레드를 보호하는 것이 중요하다.