Node.js 재택근무 환경에서 발생한 메모리 누수 추적기

문제 발견

코로나로 인해 급하게 재택근무로 전환한 지 일주일째, 프로덕션 API 서버의 메모리 사용량이 지속적으로 증가하는 이슈가 발견되었다. PM2 모니터링 결과 약 6시간마다 메모리가 1.5GB를 초과하며 재시작되고 있었다.

원격 디버깅 환경 구축

사무실 서버에 직접 접근할 수 없는 상황이라 원격으로 힙 덤프를 수집할 수 있는 환경을 먼저 구축했다.

const heapdump = require('heapdump');
const path = require('path');

app.post('/admin/heapdump', authenticate, (req, res) => {
  const filename = path.join('/tmp', `heapdump-${Date.now()}.heapsnapshot`);
  heapdump.writeSnapshot(filename, (err) => {
    if (err) return res.status(500).json({ error: err.message });
    res.json({ filename });
  });
});

원인 분석

Chrome DevTools로 heapsnapshot 파일을 분석한 결과, Socket.io 이벤트 리스너가 정리되지 않고 계속 쌓이고 있었다. 클라이언트 연결 해제 시 removeAllListeners를 호출하지 않았던 게 원인이었다.

// 문제가 있던 코드
io.on('connection', (socket) => {
  socket.on('subscribe', (room) => {
    socket.join(room);
    pubSubClient.subscribe(room, (message) => {
      socket.emit('update', message);
    });
  });
});

// 수정한 코드
io.on('connection', (socket) => {
  const handlers = new Map();
  
  socket.on('subscribe', (room) => {
    socket.join(room);
    const handler = (message) => socket.emit('update', message);
    handlers.set(room, handler);
    pubSubClient.subscribe(room, handler);
  });
  
  socket.on('disconnect', () => {
    handlers.forEach((handler, room) => {
      pubSubClient.removeListener(room, handler);
    });
    handlers.clear();
  });
});

배포 후 결과

수정 배포 후 24시간 동안 메모리 사용량을 모니터링했다. 더 이상 메모리가 증가하지 않고 400~500MB 사이에서 안정적으로 유지되었다. clinic.js로 추가 검증도 진행했다.

재택근무 환경에서도 적절한 도구만 있으면 충분히 디버깅이 가능하다는 것을 확인했다.

Node.js 재택근무 환경에서 발생한 메모리 누수 추적기