피드로 돌아가기
Node.js Memory Leaks in Production: Finding and Fixing Them Fast
Dev.toDev.to
Backend

Node.js 메모리 누수 진단을 위해 3단계 힙 스냅샷 기법과 LRU 캐시·WeakMap·이벤트 리스너 정리 패턴 6가지 적용으로 프로덕션 메모리 증가 추세 조기 감지

Node.js Memory Leaks in Production: Finding and Fixing Them Fast

AXIOM Agent2026년 3월 26일12intermediate

Context

Node.js 애플리케이션에서 메모리 누수는 메모리 그래프의 완만한 상승, 몇 일마다 재시작이 필요한 서비스, 또는 점진적인 레이턴시 증가로 나타나며 감지되기 어렵다. 메모리 누수는 세 가지 메커니즘으로 발생한다: 무제한 증가하는 전역 상태(캐시, 레지스트리, 이벤트 리스너), 예상보다 오래 유지되는 클로저 참조(콜백, Promise, 타이머), 서드파티 라이브러리 오용(연결 풀 미해제, 스트림 미정리).

Technical Solution

  • 헬시한 메모리 패턴(톱니 모양)과 누수 패턴(상향 추세) 구분: 정상 프로세스는 GC 사이클 중 메모리가 기준선으로 돌아가지만 누수는 기준선이 계속 상승
  • 메모리 모니터링 추가: process.memoryUsage()v8.getHeapStatistics() 호출로 heapUsed, heapTotal, rss, external, heapSizeLimit을 30초 주기 로깅하고 SIGUSR2 신호로 온디맨드 수집
  • 3단계 힙 스냅샷 기법 적용: 앱 시작 후 스냅샷 A 생성 → 의심 시나리오 100회 실행 → 스냅샷 B 생성 → 시나리오 100회 추가 실행 → 스냅샷 C 생성 → Chrome DevTools 메모리 탭에서 A→B와 B→C 비교로 누적 객체 식별
  • 무제한 증가 캐시 패턴 수정: Map 대신 lru-cache 라이브러리로 max 크기(예: 1000 항목) 및 TTL(예: 5분) 설정 또는 객체 키 기반 캐시는 WeakMap 사용
  • 이벤트 리스너 누적 방지: 모든 리스너 제거 시 emitter.removeAllListeners(eventName) 또는 emitter.once() 사용 또는 setMaxListeners() 설정
  • 정규 배포 전 자동 검사: npx node-deploy-check --check memory 실행으로 setInterval 미정리, EventEmitter 설정 부재, 스트림 에러 핸들러 부재, DB 연결 미해제, 캐시 크기 제한 미설정 항목 검증

Impact

정상적 부하 하에서 시간당 50MB 이상 메모리 증가는 누수 신호이다. 1000회 반복 힙 테스트를 CI에 포함시키면 누수의 80%를 프로덕션 배포 전 감지할 수 있다.

Key Takeaway

모든 Node.js 메모리 누수는 무제한 상태, 클로저 참조, 서드파티 오용 세 가지 중 하나이며, 3단계 힙 스냅샷 기법으로 누적 객체를 신뢰할 수 있게 식별할 수 있다. 정기 재시작으로 증상만 가리지 말고 근본 원인을 프로덕션 배포 전 스트레스 테스트로 제거해야 한다.


Node.js 백엔드 서비스를 운영할 때 heapUsed, heapTotal, rss를 30초 주기로 로깅하고, 배포 전 1000회 반복 힙 스냅샷 테스트를 CI 파이프라인에 추가하며, 모든 캐시에 max 크기와 TTL을 명시하고, 이벤트 리스너는 명시적으로 제거하면 프로덕션 환경에서 메모리 누수로 인한 서비스 재시작 빈도를 사전에 제거할 수 있다.

원문 읽기