Node.js 메모리 누수 추적 및 해결

문제 상황

운영 중인 Node.js API 서버의 메모리 사용량이 12시간마다 1GB씩 증가하는 현상이 발견됐다. 결국 OOM으로 프로세스가 종료되어 재시작되는 패턴이 반복됐다.

원인 분석

heapdump 패키지를 설치해 메모리 스냅샷을 주기적으로 생성했다.

const heapdump = require('heapdump');

setInterval(() => {
  const filename = `heap-${Date.now()}.heapsnapshot`;
  heapdump.writeSnapshot(filename);
  console.log(`Heap snapshot written to ${filename}`);
}, 3600000); // 1시간마다

Chrome DevTools에서 스냅샷을 비교 분석한 결과, EventEmitter의 리스너가 제거되지 않고 계속 쌓이고 있었다.

해결

문제는 WebSocket 연결 처리 로직에 있었다. 연결이 끊길 때 이벤트 리스너를 제거하지 않았다.

// Before
function handleConnection(ws) {
  ws.on('message', handleMessage);
  ws.on('close', () => {
    // cleanup 없음
  });
}

// After
function handleConnection(ws) {
  const messageHandler = (data) => handleMessage(ws, data);
  
  ws.on('message', messageHandler);
  ws.on('close', () => {
    ws.removeListener('message', messageHandler);
    // 기타 정리 작업
  });
}

추가로 --max-old-space-size 옵션으로 메모리 제한을 명시하고, PM2의 max_memory_restart 설정을 추가해 안전장치를 마련했다.

결과

배포 후 메모리 사용량이 안정화됐다. 피크 시간대에도 500MB 이하로 유지되며, 더 이상 재시작이 발생하지 않는다.

메모리 프로파일링은 정기적으로 수행할 필요가 있다는 것을 체감했다.