Node.js 스트림으로 대용량 CSV 파일 처리하기
문제 상황
데이터 분석팀에서 15GB 크기의 CSV 파일을 데이터베이스에 적재해야 한다는 요청이 들어왔다. 기존 코드는 fs.readFileSync로 전체 파일을 메모리에 올리는 방식이었고, 당연히 메모리 부족으로 프로세스가 죽었다.
해결 방법
Node.js의 스트림 API를 활용해 파일을 청크 단위로 읽고 처리하도록 변경했다. csv-parser 라이브러리와 함께 사용하면 간단하게 구현할 수 있다.
const fs = require('fs');
const csv = require('csv-parser');
const { pipeline } = require('stream');
let batchCount = 0;
let batch = [];
const BATCH_SIZE = 1000;
const processStream = fs.createReadStream('large-file.csv')
.pipe(csv())
.on('data', async (row) => {
batch.push(row);
if (batch.length >= BATCH_SIZE) {
processStream.pause();
await saveToDB(batch);
batch = [];
batchCount++;
console.log(`Processed ${batchCount * BATCH_SIZE} rows`);
processStream.resume();
}
})
.on('end', async () => {
if (batch.length > 0) {
await saveToDB(batch);
}
console.log('Complete');
})
.on('error', (err) => {
console.error('Stream error:', err);
});
async function saveToDB(rows) {
// Bulk insert logic
await db.collection('data').insertMany(rows);
}
주의사항
-
백프레셔 처리:
pause()와resume()으로 DB 저장이 완료될 때까지 읽기를 중단해야 한다. 그렇지 않으면 메모리가 계속 쌓인다. -
에러 핸들링: 스트림의 각 단계에서 에러가 발생할 수 있으므로
.on('error')핸들러를 반드시 등록해야 한다. -
배치 크기: DB 성능과 메모리를 고려해 적절한 배치 크기를 설정해야 한다. 우리 경우 1000개가 최적이었다.
결과
- 메모리 사용량: 15GB → 80MB
- 처리 시간: 약 25분 (네트워크 상황에 따라 변동)
- 안정성: 여러 번 테스트했을 때 메모리 이슈 없이 완료
스트림은 Node.js의 강력한 기능 중 하나지만 평소에 자주 사용하지 않아 낯설었다. 이번 기회에 제대로 이해하게 되었고, 대용량 파일 처리가 필요한 다른 작업에도 적용할 계획이다.