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 인터페이스가 자동으로 처리해줬다.