Node.js 스트림 백프레셔 처리 제대로 하기
문제 상황
파일 업로드 후 S3에 전송하는 API에서 500MB 이상 파일 처리 시 메모리 사용량이 2GB를 넘어가며 OOM이 발생했다. 스트림을 사용하고 있었지만 제대로 된 백프레셔 처리가 없었다.
기존 코드는 이랬다:
const stream = fs.createReadStream(filePath);
stream.on('data', async (chunk) => {
await uploadChunk(chunk); // 비동기 처리
});
문제는 data 이벤트 핸들러가 async 함수라는 점이었다. 업로드가 느려도 스트림은 계속 데이터를 읽어 메모리에 쌓였다.
해결 방법
Node.js 15부터 안정화된 pipeline과 Transform 스트림을 활용했다.
const { pipeline } = require('stream/promises');
const { Transform } = require('stream');
const uploadTransform = new Transform({
async transform(chunk, encoding, callback) {
try {
await uploadChunk(chunk);
callback();
} catch (err) {
callback(err);
}
}
});
await pipeline(
fs.createReadStream(filePath),
uploadTransform
);
Transform 스트림의 transform 콜백은 callback을 호출하기 전까지 다음 청크를 읽지 않는다. 이를 통해 자연스럽게 백프레셔가 처리된다.
추가 개선
멀티파트 업로드를 위해 청크 사이즈를 조절하는 로직도 추가했다:
const chunkTransform = new Transform({
transform(chunk, encoding, callback) {
this.buffer = this.buffer ? Buffer.concat([this.buffer, chunk]) : chunk;
if (this.buffer.length >= 5 * 1024 * 1024) {
this.push(this.buffer);
this.buffer = null;
}
callback();
},
flush(callback) {
if (this.buffer) this.push(this.buffer);
callback();
}
});
결과적으로 메모리 사용량은 100MB 이하로 유지되며 안정적으로 동작한다. 스트림을 쓴다고 끝이 아니라 백프레셔를 제대로 이해하고 구현하는 것이 중요했다.