Node.js 스트림으로 대용량 CSV 파일 처리 최적화
문제 상황
고객사 주문 데이터를 분석하는 배치 작업에서 메모리 부족으로 프로세스가 중단되는 문제가 발생했다. 파일 크기가 12GB에 달하는 CSV를 fs.readFileSync로 한 번에 읽어 처리하던 방식이 원인이었다.
해결 방법
Node.js의 스트림 API를 사용해 파일을 청크 단위로 읽고 처리하도록 변경했다.
기존 코드
const fs = require('fs');
const csv = require('csv-parser');
// 메모리에 전체 파일 로드
const data = fs.readFileSync('orders.csv', 'utf-8');
const rows = data.split('\n').map(line => parseCSV(line));
rows.forEach(row => processOrder(row));
개선 코드
const fs = require('fs');
const csv = require('csv-parser');
const { Transform } = require('stream');
const processStream = new Transform({
objectMode: true,
transform(row, encoding, callback) {
processOrder(row)
.then(() => callback())
.catch(err => callback(err));
}
});
fs.createReadStream('orders.csv')
.pipe(csv())
.pipe(processStream)
.on('finish', () => console.log('완료'))
.on('error', err => console.error('오류:', err));
성능 개선
- 메모리 사용량: 8GB → 400MB
- 처리 시간: 25분 → 18분
- 안정성: OOM 오류 해결
추가 최적화
배압(backpressure) 제어를 위해 highWaterMark 옵션을 조정했다.
fs.createReadStream('orders.csv', { highWaterMark: 64 * 1024 })
또한 병렬 처리가 필요한 경우를 위해 p-limit을 활용했다.
const pLimit = require('p-limit');
const limit = pLimit(10);
const processStream = new Transform({
objectMode: true,
async transform(row, encoding, callback) {
await limit(() => processOrder(row));
callback();
}
});
결론
대용량 파일 처리에는 스트림이 필수다. 메모리 효율은 물론 처리 속도도 향상되었다. 다만 에러 핸들링과 배압 관리에 신경 써야 한다는 점을 배웠다.