Node.js 스트림으로 대용량 CSV 파일 처리하기
문제 상황
고객사로부터 받은 5GB 크기의 CSV 파일을 파싱하여 DB에 저장해야 했다. 처음에는 fs.readFile()로 전체를 읽었다가 서버 메모리가 부족해 프로세스가 죽는 문제가 발생했다.
Stream을 이용한 해결
Node.js의 Stream API를 사용하면 파일을 청크 단위로 읽어 처리할 수 있다. csv-parser 라이브러리와 조합했다.
const fs = require('fs');
const csv = require('csv-parser');
const { promisify } = require('util');
const pipeline = promisify(require('stream').pipeline);
async function processLargeCSV(filePath) {
let count = 0;
const batchSize = 1000;
let batch = [];
const stream = fs.createReadStream(filePath)
.pipe(csv())
.on('data', async (row) => {
batch.push(row);
if (batch.length >= batchSize) {
stream.pause();
await insertBatch(batch);
batch = [];
stream.resume();
}
count++;
if (count % 10000 === 0) {
console.log(`Processed ${count} rows`);
}
})
.on('end', async () => {
if (batch.length > 0) {
await insertBatch(batch);
}
console.log(`Total: ${count} rows`);
});
}
async function insertBatch(rows) {
// Bulk insert 로직
await db.collection('data').insertMany(rows);
}
결과
메모리 사용량이 200MB 이하로 유지되면서 안정적으로 처리되었다. 5GB 파일 처리에 약 15분 정도 소요됐다.
pause()와 resume()을 활용한 백프레셔 처리가 핵심이었다. DB 삽입 속도보다 파일 읽기 속도가 빠르면 메모리에 쌓이는 문제가 있었는데, 이를 제어할 수 있었다.
참고사항
highWaterMark옵션으로 버퍼 크기 조절 가능- 에러 처리를 위해
.on('error')핸들러 필수 - Transform Stream을 만들면 더 깔끔한 구조 가능