5.7 KiB
5.7 KiB
Node.js 프로덕션 메모리 문제 해결
📌 Brief Summary
Node.js는 단일 프로세스로 장기간 실행되는 런타임이므로, 코드 내에서 참조가 제대로 해제되지 않은 객체가 누적되면 V8 힙(Heap) 메모리가 점진적으로 고갈되어 궁극적으로 OOM(Out of Memory) 크래시가 발생할 수 있습니다 [1-3]. 프로덕션 환경에서의 메모리 문제 해결은 정상적인 가비지 컬렉션(GC) 패턴과 누수 패턴을 구분하고, 타임라인 및 힙 스냅샷 분석을 통해 누수 객체의 보존 경로(Retaining Path)를 추적하여 근본 원인을 찾아 수정하는 체계적인 과정을 의미합니다 [4-8].
📖 Core 소스 Content
-
메모리 누수의 증상 및 패턴 식별:
- 정상적인 Node.js 프로세스의 메모리는 요청이 몰릴 때 힙이 증가하고 가비지 컬렉션(GC) 이후 원래의 기준선으로 돌아오는 '톱니바퀴(Sawtooth)' 패턴을 보입니다 [4, 9].
- 반면, 누수가 발생한 프로세스는 GC가 실행된 후에도 기준선으로 메모리가 떨어지지 않고 지속적으로 상승하는 '라쳇(Ratchet)' 패턴을 보입니다 [4, 9].
- 주요 증상으로는 몇 시간에 걸친 점진적인 힙 증가, 잦고 긴 GC 일시 정지(GC Pauses), 점진적인 응답 시간 증가 및 결국 프로세스가 재시작되거나 OOM으로 다운되는 현상 등이 있습니다 [1, 10].
-
메모리 모니터링 및 튜닝:
process.memoryUsage(): Node.js 프로세스의 메모리 사용량을 모니터링할 수 있으며, 전체 할당량인rss, 힙 크기인heapTotal, 실제 사용 중인heapUsed, C++ 바인딩 등이 사용하는external등의 지표를 제공합니다 [11, 12].- CLI 플래그 튜닝:
--max-old-space-size를 통해 장기 생존 객체가 저장되는 Old Space의 크기를 늘리거나,--max-semi-space-size를 조정하여 잦은 가비지 컬렉션(Scavenge)에 의한 성능 저하를 방지할 수 있습니다 [13, 14].--trace-gc플래그를 사용하면 GC 이벤트 발생 이유와 메모리 회수량, 소요 시간 등을 콘솔에 기록하여 GC 동작 상태를 상세히 파악할 수 있습니다 [15, 16].
-
주요 메모리 누수 원인 7가지:
- 이벤트 리스너 누적:
on('event', fn)을 통해 리스너를 추가한 뒤 제거하지 않는 경우입니다. 단일 이벤트 에미터에 10개 이상의 리스너가 등록될 때 발생하는MaxListenersExceededWarning은 프로덕션에서 확정적인 누수 신호로 간주됩니다 [17-19]. - 클로저(Closure) 변수 보존: 비동기 체인 내부에서 클로저가 실행 범위 내의 대규모 변수나 전체 요청/응답 객체에 대한 참조를 유지하여 GC 대상에서 제외되는 현상입니다 [17, 19].
- 무제한 캐시 증가: 크기 제한 메커니즘(예: LRU 캐시)이 없는 인메모리 캐시가 무한정 커지는 현상입니다 [19].
- 타이머 및 인터벌 표류:
clearInterval호출 없이setInterval을 방치하면 콜백 내 클로저가 무기한 보존됩니다 [20, 21]. - 순환 참조: 복잡한 객체나 C++ 네이티브 바인딩,
WeakRef의 잘못된 사용으로 인해 GC가 혼동을 일으키는 경우입니다 [21]. - 닫히지 않은 스트림과 소켓: 응답 본문을 완전히 소비하지 않거나 파괴(
destroy())하지 않은 스트림이 메모리와 네트워크 핸들을 쥐고 있는 경우입니다 [21]. - AsyncLocalStorage 컨텍스트 누수: 컨텍스트 스토어가 클린업되지 않고 계속 커지는 경우 발생합니다 [22].
- 이벤트 리스너 누적:
-
문제 해결 워크플로우 (Runbook):
- 패턴 확인: 힙 증가율을 확인하여 라쳇 패턴을 통해 누수를 확정합니다 [6].
- 스냅샷 생성 및 부하 테스트: 트래픽이 없는 상태에서 기준 힙 스냅샷(Baseline Snapshot)을 찍고, 부하를 발생시킨 뒤 두 번째 스냅샷을 캡처합니다 [4, 6]. Chrome DevTools,
heapdump라이브러리 등을 통해.heapsnapshot을 추출할 수 있습니다 [5, 18, 23, 24]. - 스냅샷 비교 분석: Chrome DevTools의 Memory 탭에서 'Comparison' 뷰를 통해 두 스냅샷 사이에 새로 할당되고 해제되지 않은 객체를 찾아냅니다 [6, 23-26].
- 보존 경로(Retaining Path) 추적: 식별된 누수 객체를 클릭하여 'Retainers' 트리를 확인하고, 해당 객체를 전역(Global) 루트로부터 붙잡고 있는 연결고리를 역추적합니다 [6, 8, 24].
- 패턴 수정 및 검증: 누수 원인을 7가지 패턴 중 하나로 매칭하여 수정한 뒤, 다시 부하 테스트를 거쳐 GC 이후 힙 메모리가 기준선으로 회복되는지 검증합니다 [6, 27].
🔗 Knowledge Connections
- Related Topics: Garbage Collection (V8), Heap Snapshot, Memory Leak Patterns, Orinoco Garbage Collector
- Projects/Contexts: Chrome DevTools Memory Panel, Node.js Production Monitoring
- Contradictions/Notes: 가비지 컬렉션(GC)은 애플리케이션의 힙 메모리를 정리해주지만, 메인 스레드 실행을 멈추는 'stop-the-world' 특성을 지닙니다. V8은 Orinoco 프로젝트를 통해 병렬(Parallel), 점진적(Incremental), 동시적(Concurrent) 처리 기법을 도입하여 지연(Pause) 시간을 최소화했지만 [28-32], 개발자가
--expose-gc를 활성화하여global.gc()를 수동으로 강제 호출하는 것은 시스템 성능을 악화시킬 수 있으므로 매우 주의해서 사용해야 한다고 경고하고 있습니다 [33, 34].
Last updated: 2026-04-19