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 같은 경우 더욱 주의가 필요하다.

Node.js 서버에서 메모리 누수 추적하기