Node.js 서버에서 메모리 누수 추적하기
문제 상황
운영 중인 Express 서버의 메모리 사용량이 시간이 지날수록 계속 증가했다. 배포 직후 200MB였던 메모리가 2~3일 뒤 1.5GB를 넘어서며 OOM 에러로 재시작되는 상황이 반복됐다.
원인 추적
heapdump 패키지를 사용해 힙 스냅샷을 주기적으로 생성했다.
const heapdump = require('heapdump');
app.get('/heapdump', (req, res) => {
const filename = `heap-${Date.now()}.heapsnapshot`;
heapdump.writeSnapshot(filename, (err) => {
if (err) return res.status(500).send(err.message);
res.send(`Heap dump written to ${filename}`);
});
});
Chrome DevTools의 Memory Profiler로 스냅샷을 비교 분석한 결과, WebSocket 연결과 관련된 이벤트 리스너가 제거되지 않고 계속 쌓이고 있었다.
해결 방법
문제는 WebSocket 연결 종료 시 이벤트 리스너를 정리하지 않은 것이었다.
// Before
wss.on('connection', (ws) => {
const handler = (data) => processData(data, ws);
dataEmitter.on('update', handler);
});
// After
wss.on('connection', (ws) => {
const handler = (data) => processData(data, ws);
dataEmitter.on('update', handler);
ws.on('close', () => {
dataEmitter.removeListener('update', handler);
});
});
수정 후 메모리 사용량이 안정적으로 유지됐다. process.memoryUsage()로 모니터링 지표도 추가했다.
교훈
EventEmitter를 사용할 때는 항상 리스너 정리를 고려해야 한다. 특히 장시간 연결이 유지되는 WebSocket, SSE 같은 경우 더욱 주의가 필요하다.