Node.js 스트림으로 대용량 CSV 파일 처리 최적화

문제 상황

고객사 데이터 마이그레이션 작업 중 5GB 크기의 CSV 파일을 파싱해서 DB에 insert해야 했다. 초기에는 fs.readFile로 전체를 읽어서 처리했는데, 메모리 부족으로 프로세스가 죽는 문제가 발생했다.

해결 방법

Node.js의 스트림 API를 사용해 청크 단위로 처리하도록 변경했다. csv-parser 라이브러리와 조합해서 구현했다.

const fs = require('fs');
const csv = require('csv-parser');
const { Transform } = require('stream');

const batchSize = 1000;
let batch = [];

const batchTransform = new Transform({
  objectMode: true,
  async transform(row, encoding, callback) {
    batch.push(row);
    
    if (batch.length >= batchSize) {
      const currentBatch = [...batch];
      batch = [];
      
      try {
        await db.insertMany(currentBatch);
        callback();
      } catch (error) {
        callback(error);
      }
    } else {
      callback();
    }
  },
  async flush(callback) {
    if (batch.length > 0) {
      await db.insertMany(batch);
    }
    callback();
  }
});

fs.createReadStream('large-data.csv')
  .pipe(csv())
  .pipe(batchTransform)
  .on('finish', () => console.log('완료'))
  .on('error', (err) => console.error(err));

결과

  • 메모리 사용량: 2.5GB → 120MB로 감소
  • 처리 시간: 약간 증가했지만 안정성 확보
  • 배치 단위로 DB insert해서 쿼리 수도 최적화

추가 고려사항

백프레셔(backpressure) 처리도 중요했다. DB insert 속도가 파일 읽기 속도보다 느릴 때 메모리가 쌓이는 문제가 있었는데, highWaterMark 옵션으로 버퍼 크기를 제한해서 해결했다.

스트림은 러닝 커브가 있지만, 대용량 데이터 처리에서는 필수적인 도구라는 걸 다시 한번 확인했다.