Node.js 스트림으로 대용량 CSV 파싱 성능 개선하기
문제 상황
데이터 분석팀에서 매일 생성되는 200MB 크기의 CSV 파일을 처리하는 배치 작업이 있었다. 기존에는 fs.readFileSync로 전체 파일을 메모리에 로드한 뒤 파싱했는데, 파일 크기가 커지면서 힙 메모리 부족으로 프로세스가 종료되는 문제가 발생했다.
기존 코드
const fs = require('fs');
const parse = require('csv-parse/lib/sync');
const data = fs.readFileSync('large-file.csv', 'utf-8');
const records = parse(data, { columns: true });
records.forEach(record => {
// DB 저장 로직
});
메모리 사용량이 약 2GB까지 증가했고, 파일이 더 커지면 OOM이 발생했다.
스트림 기반으로 전환
csv-parse 라이브러리의 스트림 API를 사용하여 청크 단위로 처리하도록 변경했다.
const fs = require('fs');
const { parse } = require('csv-parse');
const { pipeline } = require('stream/promises');
const parser = parse({
columns: true,
skip_empty_lines: true
});
const processRecord = async (record) => {
// DB 저장 로직
await db.insert(record);
};
const writableStream = new Writable({
objectMode: true,
async write(record, encoding, callback) {
try {
await processRecord(record);
callback();
} catch (error) {
callback(error);
}
}
});
await pipeline(
fs.createReadStream('large-file.csv'),
parser,
writableStream
);
결과
- 메모리 사용량: 2GB → 200MB (약 1/10 감소)
- 처리 시간: 45초 → 32초 (배압 제어 덕분에 DB 부하도 분산)
- 500MB 파일도 안정적으로 처리 가능
배운 점
Node.js 스트림은 학습 곡선이 있지만, 대용량 데이터 처리에서는 필수다. pipeline을 사용하면 에러 핸들링과 백프레셔를 자동으로 처리해준다. 앞으로 파일 처리는 기본적으로 스트림을 고려해야겠다.