Node.js 스트림으로 대용량 CSV 파일 처리 메모리 최적화
문제 상황
데이터 마이그레이션 작업 중 10GB 크기의 CSV 파일을 처리하는 배치 작업이 필요했다. 초기에는 fs.readFile로 전체 파일을 메모리에 올려 처리했는데, 로컬에서는 문제없었지만 운영 서버(메모리 2GB)에서는 OOM 에러가 발생했다.
기존 코드의 문제
const data = await fs.promises.readFile('large-file.csv', 'utf-8');
const lines = data.split('\n');
for (const line of lines) {
await processLine(line);
}
전체 파일을 메모리에 올리기 때문에 파일 크기만큼 메모리를 차지했다.
스트림 기반으로 전환
const fs = require('fs');
const readline = require('readline');
const processCSV = async (filePath) => {
const fileStream = fs.createReadStream(filePath);
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
});
let processedCount = 0;
for await (const line of rl) {
await processLine(line);
processedCount++;
if (processedCount % 10000 === 0) {
console.log(`Processed ${processedCount} lines`);
}
}
};
readline 모듈을 사용해 한 줄씩 읽어 처리하도록 변경했다. 메모리 사용량이 약 50MB로 일정하게 유지되었다.
백프레셔 처리
처음에는 DB 쓰기 속도보다 읽기가 빨라 커넥션 풀이 고갈되는 문제가 있었다. 스트림의 pause/resume으로 백프레셔를 구현했다.
const stream = fs.createReadStream(filePath, { highWaterMark: 64 * 1024 });
stream.on('data', async (chunk) => {
stream.pause();
await processChunk(chunk);
stream.resume();
});
결과
- 메모리 사용량: 8GB → 50MB
- 처리 시간: 약간 증가(45분 → 52분)했지만 안정성 확보
- 파일 크기에 관계없이 일정한 메모리 사용
스트림은 Node.js의 핵심 패턴이지만 매번 필요할 때마다 찾아보게 된다. 이번 기회에 제대로 정리했다.