Node.js 스트림으로 대용량 CSV 파일 처리하기

문제 상황

레거시 시스템에서 약 10GB 크기의 사용자 데이터 CSV를 새 DB로 마이그레이션해야 했다. 초기에 fs.readFile로 전체를 읽어 처리하려 했으나 메모리 부족으로 프로세스가 죽었다.

해결 방법

Node.js의 스트림 API를 사용해 청크 단위로 처리하도록 변경했다.

const fs = require('fs');
const readline = require('readline');
const { Pool } = require('pg');

const pool = new Pool({
  // DB 설정
});

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

  let batch = [];
  const BATCH_SIZE = 1000;

  for await (const line of rl) {
    const userData = parseCSVLine(line);
    batch.push(userData);

    if (batch.length >= BATCH_SIZE) {
      await insertBatch(batch);
      batch = [];
    }
  }

  if (batch.length > 0) {
    await insertBatch(batch);
  }
};

const insertBatch = async (batch) => {
  const client = await pool.connect();
  try {
    await client.query('BEGIN');
    for (const user of batch) {
      await client.query(
        'INSERT INTO users(name, email) VALUES($1, $2)',
        [user.name, user.email]
      );
    }
    await client.query('COMMIT');
  } catch (e) {
    await client.query('ROLLBACK');
    throw e;
  } finally {
    client.release();
  }
};

결과

  • 메모리 사용량: 150MB 이하로 안정적 유지
  • 처리 시간: 약 25분 (2000만 행)
  • 배치 처리로 DB 부하 분산

개선 포인트

COPY 명령을 사용하면 더 빠를 것 같지만, 데이터 변환 로직이 복잡해 우선 이 방식으로 진행했다. 다음에는 변환 로직을 최적화해서 COPY를 활용해볼 예정이다.

스트림은 대용량 파일, 실시간 데이터 처리에서 필수적이다. backpressure 처리도 중요한데 이번에는 readline 인터페이스가 자동으로 처리해줬다.

Node.js 스트림으로 대용량 CSV 파일 처리하기