Node.js 스트림으로 대용량 CSV 파싱 메모리 최적화
문제 상황
고객사 데이터 마이그레이션 작업 중 10GB 규모의 CSV 파일을 파싱하던 배치 서버가 OOM으로 죽는 현상이 반복됐다. 기존 코드는 fs.readFileSync로 파일 전체를 메모리에 올린 뒤 처리하는 구조였다.
const data = fs.readFileSync('large-file.csv', 'utf-8');
const rows = data.split('\n');
rows.forEach(row => processRow(row));
해결 방법
Stream API와 csv-parser 라이브러리를 조합해 청크 단위로 읽고 처리하도록 변경했다.
const fs = require('fs');
const csv = require('csv-parser');
let processedCount = 0;
fs.createReadStream('large-file.csv')
.pipe(csv())
.on('data', async (row) => {
await processRow(row);
processedCount++;
if (processedCount % 10000 === 0) {
console.log(`Processed: ${processedCount}`);
}
})
.on('end', () => {
console.log('Complete:', processedCount);
})
.on('error', (err) => {
console.error('Error:', err);
});
성능 개선
- 메모리 사용량: 8GB → 400MB
- 처리 시간: 약 15% 단축 (I/O 병렬 처리 효과)
- 안정성: OOM 오류 완전 해소
추가 고려사항
비동기 처리가 너무 빨라 DB 커넥션 풀이 고갈되는 문제가 있었다. p-limit 라이브러리로 동시 처리 개수를 제한했다.
const pLimit = require('p-limit');
const limit = pLimit(10);
fs.createReadStream('large-file.csv')
.pipe(csv())
.on('data', (row) => {
limit(() => processRow(row));
});
대용량 파일 처리에서는 스트림이 필수다. 메모리 효율뿐 아니라 backpressure 제어까지 고려하면 더 안정적인 시스템을 만들 수 있다.