# [[Node.js 프로덕션 메모리 문제 해결|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가지:** 1. **이벤트 리스너 누적:** `on('event', fn)`을 통해 리스너를 추가한 뒤 제거하지 않는 경우입니다. 단일 이벤트 에미터에 10개 이상의 리스너가 등록될 때 발생하는 `MaxListenersExceededWarning`은 프로덕션에서 확정적인 누수 신호로 간주됩니다 [17-19]. 2. **클로저(Closure) 변수 보존:** 비동기 체인 내부에서 클로저가 실행 범위 내의 대규모 변수나 전체 요청/응답 객체에 대한 참조를 유지하여 GC 대상에서 제외되는 현상입니다 [17, 19]. 3. **무제한 캐시 증가:** 크기 제한 메커니즘(예: LRU 캐시)이 없는 인메모리 캐시가 무한정 커지는 현상입니다 [19]. 4. **타이머 및 인터벌 표류:** `clearInterval` 호출 없이 `setInterval`을 방치하면 콜백 내 클로저가 무기한 보존됩니다 [20, 21]. 5. **순환 참조:** 복잡한 객체나 C++ 네이티브 바인딩, `WeakRef`의 잘못된 사용으로 인해 GC가 혼동을 일으키는 경우입니다 [21]. 6. **닫히지 않은 스트림과 소켓:** 응답 본문을 완전히 소비하지 않거나 파괴(`destroy()`)하지 않은 스트림이 메모리와 네트워크 핸들을 쥐고 있는 경우입니다 [21]. 7. **AsyncLocalStorage 컨텍스트 누수:** 컨텍스트 스토어가 클린업되지 않고 계속 커지는 경우 발생합니다 [22]. * **문제 해결 워크플로우 (Runbook):** 1. **패턴 확인:** 힙 증가율을 확인하여 라쳇 패턴을 통해 누수를 확정합니다 [6]. 2. **스냅샷 생성 및 부하 테스트:** 트래픽이 없는 상태에서 기준 힙 스냅샷(Baseline Snapshot)을 찍고, 부하를 발생시킨 뒤 두 번째 스냅샷을 캡처합니다 [4, 6]. Chrome DevTools, `heapdump` 라이브러리 등을 통해 `.heapsnapshot`을 추출할 수 있습니다 [5, 18, 23, 24]. 3. **스냅샷 비교 분석:** Chrome DevTools의 Memory 탭에서 'Comparison' 뷰를 통해 두 스냅샷 사이에 새로 할당되고 해제되지 않은 객체를 찾아냅니다 [6, 23-26]. 4. **보존 경로(Retaining Path) 추적:** 식별된 누수 객체를 클릭하여 'Retainers' 트리를 확인하고, 해당 객체를 전역(Global) 루트로부터 붙잡고 있는 연결고리를 역추적합니다 [6, 8, 24]. 5. **패턴 수정 및 검증:** 누수 원인을 7가지 패턴 중 하나로 매칭하여 수정한 뒤, 다시 부하 테스트를 거쳐 GC 이후 힙 메모리가 기준선으로 회복되는지 검증합니다 [6, 27]. ## 🔗 Knowledge Connections - **Related Topics:** Garbage Collection (V8), [[Heap Snapshot|Heap Snapshot]], Memory Leak Patterns, Orinoco Garbage Collector - **Projects/Contexts:** [[Chrome DevTools Memory Panel|Chrome DevTools Memory Panel]], [[Node.js Production Monitoring|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*