Node.js 스트림으로 대용량 CSV 파일 처리하기

문제 상황

데이터 마이그레이션 작업 중 12GB 크기의 CSV 파일을 파싱해서 데이터베이스에 삽입해야 했다. 처음에는 fs.readFile()로 전체를 읽어서 처리하려 했는데, 당연히 메모리 부족으로 프로세스가 죽었다.

스트림 기반 해결

Node.js의 스트림을 사용하면 파일을 청크 단위로 읽으면서 처리할 수 있다. csv-parser 라이브러리와 조합했다.

const fs = require('fs');
const csv = require('csv-parser');
const { pipeline } = require('stream');
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 (err) {
        callback(err);
      }
    } else {
      callback();
    }
  },
  async flush(callback) {
    if (batch.length > 0) {
      try {
        await db.insertMany(batch);
        callback();
      } catch (err) {
        callback(err);
      }
    } else {
      callback();
    }
  }
});

pipeline(
  fs.createReadStream('large-file.csv'),
  csv(),
  batchTransform,
  (err) => {
    if (err) {
      console.error('처리 실패:', err);
    } else {
      console.log('완료');
    }
  }
);

주요 포인트

백프레셔 처리

스트림은 자동으로 백프레셔를 처리한다. DB 삽입이 느려지면 파일 읽기 속도도 자동으로 조절된다.

배치 처리

한 줄씩 DB에 삽입하면 너무 느리다. 1000개씩 묶어서 insertMany()로 처리하니 속도가 10배 이상 빨라졌다.

에러 핸들링

pipeline()을 사용하면 에러가 발생했을 때 자동으로 모든 스트림을 정리해준다. 직접 pipe()를 체이닝하는 것보다 안전하다.

결과

12GB 파일을 약 15분만에 처리했고, 메모리 사용량은 150MB 내외로 일정하게 유지됐다. 스트림의 위력을 제대로 느낀 작업이었다.

Node.js 스트림으로 대용량 CSV 파일 처리하기