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

문제 상황

월말 정산 배치 작업이 실패하기 시작했다. 거래 데이터가 쌓이면서 CSV 파일 크기가 5GB를 넘어섰고, fs.readFile로 한 번에 읽다가 메모리 부족으로 프로세스가 죽었다.

// 기존 코드 - 메모리 부족 발생
const data = await fs.readFile('transactions.csv', 'utf-8');
const rows = data.split('\n');
for (const row of rows) {
  await processTransaction(row);
}

스트림 기반 처리로 전환

Node.js의 스트림 API를 사용해 청크 단위로 읽고 처리하도록 변경했다. csv-parser 라이브러리와 pipeline을 활용했다.

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

const processStream = new Transform({
  objectMode: true,
  async transform(row, encoding, callback) {
    try {
      await processTransaction(row);
      callback();
    } catch (err) {
      callback(err);
    }
  }
});

await pipeline(
  fs.createReadStream('transactions.csv'),
  csv(),
  processStream
);

결과

  • 메모리 사용량: 4.8GB → 450MB (90% 감소)
  • 처리 시간: 23분 → 19분 (약간 개선)
  • OOM 에러 해결

처음에는 async/await와 스트림을 섞는 게 낯설었지만, Transform 스트림의 objectMode를 활용하니 기존 로직을 거의 그대로 유지할 수 있었다.

백프레셔(backpressure) 처리도 자동으로 되어서 DB 쓰기 속도에 맞춰 파일 읽기가 조절되는 것도 장점이었다. 앞으로 대용량 파일 처리는 무조건 스트림부터 고려해야겠다.