Node.js 스트림으로 대용량 CSV 파싱 메모리 이슈 해결

문제 상황

고객사 주문 데이터를 CSV로 받아 DB에 적재하는 배치 작업을 구현했다. 초기에는 fs.readFileSync로 파일을 전부 읽어 처리했는데, 150만 건 규모의 파일에서 메모리 부족으로 프로세스가 죽는 현상이 발생했다.

// 기존 방식 - 메모리 3GB 이상 사용
const data = fs.readFileSync('orders.csv', 'utf-8');
const rows = data.split('\n');
for (const row of rows) {
  await processRow(row);
}

스트림 기반 처리로 변경

Node.js의 stream 모듈과 csv-parser 라이브러리를 사용해 청크 단위로 읽도록 수정했다.

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

await pipeline(
  fs.createReadStream('orders.csv'),
  csv(),
  async function* (source) {
    for await (const row of source) {
      await processRow(row);
      yield row;
    }
  }
);

배치 처리로 성능 개선

한 건씩 DB INSERT하면 느려서 100건 단위로 배치 처리를 추가했다.

let batch = [];
const BATCH_SIZE = 100;

await pipeline(
  fs.createReadStream('orders.csv'),
  csv(),
  async function* (source) {
    for await (const row of source) {
      batch.push(row);
      if (batch.length >= BATCH_SIZE) {
        await db.insertMany(batch);
        batch = [];
      }
      yield row;
    }
    if (batch.length > 0) {
      await db.insertMany(batch);
    }
  }
);

결과

  • 메모리 사용량: 3GB → 300MB
  • 처리 시간: 45분 → 12분
  • 150만 건 데이터 안정적 처리 가능

스트림은 대용량 파일을 다룰 때 필수다. backpressure 처리도 자동으로 되어 안정성이 높다.