# [[Node.js 프로덕션 메모리 병목 분석]]

## 📌 Brief Summary
Node.js는 단일 프로세스로 장기간 실행되는 특성이 있어, 더 이상 필요하지 않은 객체의 참조가 유지될 경우 V8 힙(Heap) 메모리가 해제되지 않고 지속적으로 누적되는 메모리 누수 현상이 발생할 수 있습니다 [1, 2]. 프로덕션 환경에서 이러한 누수는 가비지 컬렉션(GC)의 오버헤드를 늘려 애플리케이션의 응답 지연이나 OOM(Out of Memory) 크래시 같은 심각한 병목 현상을 유발합니다 [3]. 이를 분석하고 해결하기 위해 개발자는 `--trace-gc` 같은 실행 플래그, `heapdump`를 통한 힙 스냅샷(Heap Snapshot) 획득, 그리고 크롬 개발자 도구(Chrome DevTools) 등을 활용하여 지속적으로 증가하는 객체와 이를 잡아두는 유지 경로(Retaining Path)를 추적해야 합니다 [4-6].

## 📖 Core Content

*   **V8 메모리 구조와 가비지 컬렉션(GC) 메커니즘**
    Node.js의 기반인 V8 엔진은 동적 데이터를 **힙(Heap) 공간**에 할당하며, 대부분의 객체는 짧은 수명을 가진다는 '세대별 가설(Generational Hypothesis)'을 기반으로 설계되었습니다 [7-9]. 
    *   **New Space (Young Generation):** 새롭게 생성된 객체가 할당되는 공간으로, 꽉 차면 빠르게 작동하는 **스캐빈지(Scavenge, Minor GC)** 알고리즘이 발생해 불필요한 객체를 정리하고 살아남은 객체를 옮깁니다 [10-13].
    *   **Old Space:** 스캐빈지 과정을 여러 번 통과한 수명이 긴 객체들이 승격(Promotion)되어 머무는 공간입니다 [9, 10, 14]. 이곳은 **Mark-Sweep-Compact (Major GC)** 알고리즘이 작동하며 메모리 파편화를 줄이고 남은 공간을 확보하지만, 스캐빈지에 비해 실행 비용(오버헤드)이 큽니다 [15, 16].

*   **메모리 병목 및 누수의 주요 증상과 패턴**
    건강한 프로세스는 GC가 일어날 때마다 메모리 사용량이 다시 줄어드는 **톱니바퀴(Sawtooth)** 패턴을 보이지만, 메모리 누수가 발생하면 할당량이 줄어들지 않고 계속 증가하는 **라쳇(Ratchet)** 패턴이 관찰됩니다 [4]. Node.js 환경에서 주로 발생하는 **7가지 주요 누수 패턴**은 다음과 같습니다 [17-19]:
    1.  **EventEmitter 리스너 누적:** 이벤트 리스너를 계속 추가만 하고 제거하지 않아 발생하는 가장 흔한 누수로, `MaxListenersExceededWarning`이 발생하면 의심해야 합니다 [17, 18].
    2.  **클로저(Closure) 변수 유지:** 요청/응답 객체 등 거대한 변수가 클로저에 의해 캡처된 상태로 요청 수명주기 이후에도 남아있는 경우입니다 [18].
    3.  **무제한 캐시 증식:** LRU와 같은 크기 제한 로직이 없는 인메모리 캐시를 사용할 때 발생합니다 [18].
    4.  **타이머 누수:** `clearInterval` 처리 없이 `setInterval`이 계속 실행되며 클로저 내부 객체의 GC를 방해합니다 [19].
    5.  **복잡한 순환 참조:** C++ 네이티브 바인딩 또는 잘못 사용된 `WeakRef`와 결합한 복잡한 순환 참조가 GC를 방해할 수 있습니다 [19].
    6.  **종료되지 않은 스트림/소켓:** `.destroy()` 처리되지 않은 스트림이 버퍼와 네트워크 핸들을 점유합니다 [19].
    7.  **`AsyncLocalStorage` 컨텍스트 누수:** 저장소가 적절한 클린업 없이 과도하게 커지는 경우입니다 [19].

*   **프로덕션 메모리 병목 진단 및 프로파일링 도구**
    *   **`process.memoryUsage()` 모니터링:** `rss`(상주 집합 크기), `heapTotal`, `heapUsed` 메트릭을 추적하여 힙 사용량이 지속해서 증가하는지 감시할 수 있습니다 [20, 21].
    *   **GC 로그 추적 (`--trace-gc`):** 이 플래그를 활성화하면 Scavenge와 Mark-Sweep 이벤트의 발생 빈도와 소요 시간, 회수된 메모리양을 확인할 수 있습니다 [22, 23]. 두 GC 사이의 간격보다 GC 처리에 걸리는 시간이 더 크다면 심각한 메모리 병목을 겪고 있는 것입니다 [24].
    *   **힙 스냅샷(Heap Snapshot) 분석:** 운영 서버에서는 `heapdump` 패키지 등으로 스냅샷을 생성하거나 로드 테스트 시 `--inspect` 플래그를 사용해 **크롬 개발자 도구(Chrome DevTools)**와 연결할 수 있습니다 [4, 5, 17]. 개발자 도구의 '할당 타임라인(Allocation timeline)'을 통해 GC 후에도 남아있는 파란색 막대를 찾고, 스냅샷 비교(Comparison view) 기능을 사용하여 누수된 객체와 해당 객체가 참조를 유지하고 있는 경로(Retainers tree)를 짚어낼 수 있습니다 [25-30].

*   **메모리 튜닝 플래그**
    메모리 누수 자체의 해결책은 아니지만, 프로세스 특성에 맞춰 V8 엔진의 메모리 한도를 조정함으로써 병목을 완화할 수 있습니다 [31]. 
    *   `--max-old-space-size`: 롱 폴링이나 캐시 등 영구적인 데이터가 많은 앱에서 Old Space의 크기 제한(기본 제약)을 늘릴 때 사용합니다 [31].
    *   `--max-semi-space-size`: 초당 요청 수가 많아 수명이 짧은 임시 객체가 대량 생성되는 환경에서 New Space의 크기를 늘려 잦은 Minor GC 실행을 줄입니다 [32, 33].

## 🔗 Knowledge Connections
- **Related Topics:** [[V8 가비지 컬렉션 (Garbage Collection)]], [[힙 스냅샷 (Heap Snapshot)]], [[메모리 누수 (Memory Leaks)]]
- **Projects/Contexts:** [[Chrome DevTools (크롬 개발자 도구)]], [[Node.js 모니터링 및 튜닝]]
- **Contradictions/Notes:** 애플리케이션 내에서 수동으로 GC를 제어하기 위해 `--expose-gc` 플래그를 켜고 `global.gc()`를 호출할 수 있지만, 이 기능은 V8의 자동 가비지 컬렉션을 비활성화하지는 않습니다. 오히려 수동 호출의 남용은 애플리케이션의 응답 속도 등 전체적인 성능에 부정적인 영향을 미칠 수 있으므로 주의해서 사용해야 한다고 소스는 경고합니다 [34, 35].

---
*Last updated: 2026-04-19*