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

문제 상황

데이터 마이그레이션 작업 중 10GB 크기의 CSV 파일을 처리하는 배치 작업이 필요했다. 초기에는 fs.readFile로 전체 파일을 메모리에 올려 처리했는데, 로컬에서는 문제없었지만 운영 서버(메모리 2GB)에서는 OOM 에러가 발생했다.

기존 코드의 문제

const data = await fs.promises.readFile('large-file.csv', 'utf-8');
const lines = data.split('\n');
for (const line of lines) {
  await processLine(line);
}

전체 파일을 메모리에 올리기 때문에 파일 크기만큼 메모리를 차지했다.

스트림 기반으로 전환

const fs = require('fs');
const readline = require('readline');

const processCSV = async (filePath) => {
  const fileStream = fs.createReadStream(filePath);
  const rl = readline.createInterface({
    input: fileStream,
    crlfDelay: Infinity
  });

  let processedCount = 0;
  for await (const line of rl) {
    await processLine(line);
    processedCount++;
    if (processedCount % 10000 === 0) {
      console.log(`Processed ${processedCount} lines`);
    }
  }
};

readline 모듈을 사용해 한 줄씩 읽어 처리하도록 변경했다. 메모리 사용량이 약 50MB로 일정하게 유지되었다.

백프레셔 처리

처음에는 DB 쓰기 속도보다 읽기가 빨라 커넥션 풀이 고갈되는 문제가 있었다. 스트림의 pause/resume으로 백프레셔를 구현했다.

const stream = fs.createReadStream(filePath, { highWaterMark: 64 * 1024 });
stream.on('data', async (chunk) => {
  stream.pause();
  await processChunk(chunk);
  stream.resume();
});

결과

  • 메모리 사용량: 8GB → 50MB
  • 처리 시간: 약간 증가(45분 → 52분)했지만 안정성 확보
  • 파일 크기에 관계없이 일정한 메모리 사용

스트림은 Node.js의 핵심 패턴이지만 매번 필요할 때마다 찾아보게 된다. 이번 기회에 제대로 정리했다.