Node.js 스트림으로 대용량 CSV 파싱 메모리 최적화

문제 상황

고객사 데이터 마이그레이션 작업 중 10GB 규모의 CSV 파일을 파싱하던 배치 서버가 OOM으로 죽는 현상이 반복됐다. 기존 코드는 fs.readFileSync로 파일 전체를 메모리에 올린 뒤 처리하는 구조였다.

const data = fs.readFileSync('large-file.csv', 'utf-8');
const rows = data.split('\n');
rows.forEach(row => processRow(row));

해결 방법

Stream API와 csv-parser 라이브러리를 조합해 청크 단위로 읽고 처리하도록 변경했다.

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

let processedCount = 0;

fs.createReadStream('large-file.csv')
  .pipe(csv())
  .on('data', async (row) => {
    await processRow(row);
    processedCount++;
    
    if (processedCount % 10000 === 0) {
      console.log(`Processed: ${processedCount}`);
    }
  })
  .on('end', () => {
    console.log('Complete:', processedCount);
  })
  .on('error', (err) => {
    console.error('Error:', err);
  });

성능 개선

  • 메모리 사용량: 8GB → 400MB
  • 처리 시간: 약 15% 단축 (I/O 병렬 처리 효과)
  • 안정성: OOM 오류 완전 해소

추가 고려사항

비동기 처리가 너무 빨라 DB 커넥션 풀이 고갈되는 문제가 있었다. p-limit 라이브러리로 동시 처리 개수를 제한했다.

const pLimit = require('p-limit');
const limit = pLimit(10);

fs.createReadStream('large-file.csv')
  .pipe(csv())
  .on('data', (row) => {
    limit(() => processRow(row));
  });

대용량 파일 처리에서는 스트림이 필수다. 메모리 효율뿐 아니라 backpressure 제어까지 고려하면 더 안정적인 시스템을 만들 수 있다.