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

문제 상황

고객사에서 200MB 크기의 CSV 파일을 업로드하면 서버가 OOM으로 종료되는 이슈가 발생했다. 기존 코드는 파일 전체를 메모리에 로드한 후 파싱하는 방식이었다.

const csv = require('csv-parser');
const fs = require('fs');

// 기존 방식 - 전체 로드
const data = fs.readFileSync('large.csv', 'utf-8');
const parsed = csvParser(data); // 메모리 폭발

해결 방법

Node.js 스트림을 활용해 청크 단위로 처리하도록 변경했다.

const results = [];
let processedCount = 0;

fs.createReadStream('large.csv')
  .pipe(csv())
  .on('data', async (row) => {
    // DB에 배치 insert
    results.push(row);
    
    if (results.length >= 1000) {
      await batchInsert(results);
      results.length = 0;
      processedCount += 1000;
      console.log(`Processed: ${processedCount}`);
    }
  })
  .on('end', async () => {
    if (results.length > 0) {
      await batchInsert(results);
    }
  });

결과

  • 메모리 사용량: 800MB → 80MB
  • 처리 시간: 45초 → 52초 (약간 증가)
  • 안정성: 서버 크래시 없이 안정적으로 처리

1000개씩 배치로 끊어서 DB에 insert하는 방식으로 변경하니 메모리 사용량이 크게 줄었다. 처리 시간은 소폭 증가했지만 안정성이 훨씬 중요했다.

교훈

대용량 파일 처리 시 스트림을 기본으로 고려해야 한다. 특히 사용자 업로드 파일은 크기를 예측할 수 없으므로 스트림 기반 처리가 필수다.