Node.js 스트림 파이프라인으로 대용량 CSV 처리 최적화
문제 상황
매일 새벽 2시에 실행되는 배치 작업이 최근 들어 자주 실패하기 시작했다. 로그를 확인하니 JavaScript heap out of memory 에러였다. 파트너사에서 받는 CSV 파일 크기가 점점 커지면서 발생한 문제였다.
기존 코드는 fs.readFileSync로 전체 파일을 메모리에 올린 후 처리하는 방식이었다.
const data = fs.readFileSync('large-data.csv', 'utf-8');
const lines = data.split('\n');
for (const line of lines) {
await processLine(line);
}
Stream 기반 리팩토링
Node.js의 Stream API를 사용해 청크 단위로 읽고 처리하도록 변경했다. pipeline 함수를 사용하면 에러 핸들링도 간결해진다.
const { pipeline } = require('stream/promises');
const fs = require('fs');
const { Transform } = require('stream');
class LineProcessor extends Transform {
constructor() {
super({ objectMode: true });
this.buffer = '';
}
_transform(chunk, encoding, callback) {
this.buffer += chunk.toString();
const lines = this.buffer.split('\n');
this.buffer = lines.pop();
for (const line of lines) {
this.push(line);
}
callback();
}
_flush(callback) {
if (this.buffer) this.push(this.buffer);
callback();
}
}
await pipeline(
fs.createReadStream('large-data.csv'),
new LineProcessor(),
async function* (source) {
for await (const line of source) {
yield await processLine(line);
}
},
fs.createWriteStream('output.json')
);
결과
- 메모리 사용량: 800MB → 80MB
- 처리 시간: 45초 → 38초
- 안정성: OOM 에러 해결
배압(backpressure) 제어가 자동으로 이뤄져서 다운스트림이 느려도 메모리가 넘치지 않는다. pipeline은 Node 15부터 promise 버전이 생겨서 에러 핸들링이 훨씬 깔끔해졌다.
대용량 파일 처리할 때는 Stream을 기본으로 고려해야겠다는 교훈을 얻었다.