Node.js 스트림으로 대용량 CSV 파일 처리하기
문제 상황
데이터 분석팀에서 5GB 크기의 CSV 파일을 파싱해 데이터베이스에 적재하는 작업을 요청했다. 처음엔 fs.readFileSync로 파일을 통째로 읽어 처리하려 했는데, 메모리 부족으로 프로세스가 죽었다.
스트림 기반 처리
Node.js의 스트림을 사용하면 파일을 청크 단위로 읽어 처리할 수 있다. 전체 파일을 메모리에 올리지 않아도 되므로 메모리 사용량이 일정하게 유지된다.
const fs = require('fs');
const readline = require('readline');
const { Writable } = require('stream');
class DBWriteStream extends Writable {
constructor(options) {
super(options);
this.batch = [];
this.batchSize = 1000;
}
async _write(chunk, encoding, callback) {
this.batch.push(this.parseCSVLine(chunk.toString()));
if (this.batch.length >= this.batchSize) {
await this.flushBatch();
}
callback();
}
async flushBatch() {
if (this.batch.length === 0) return;
await db.batchInsert(this.batch);
this.batch = [];
}
parseCSVLine(line) {
const [id, name, email] = line.split(',');
return { id, name, email };
}
}
const readStream = fs.createReadStream('large-file.csv');
const rl = readline.createInterface({ input: readStream });
const writeStream = new DBWriteStream();
rl.on('line', (line) => {
writeStream.write(line);
});
rl.on('close', async () => {
await writeStream.flushBatch();
console.log('처리 완료');
});
성능 개선
배치 처리를 추가해 DB 인서트 횟수를 줄였다. 1000개씩 모아서 한 번에 insert하니 처리 시간이 10분에서 2분으로 단축됐다.
메모리 사용량은 약 50MB로 일정하게 유지됐고, 더 큰 파일도 동일한 방식으로 처리할 수 있게 됐다.
교훈
대용량 파일 처리 시 스트림을 기본으로 고려해야 한다. 배치 처리를 통해 I/O 오버헤드를 줄이는 것도 중요하다.