6.2 KiB
6.2 KiB
Node.js 프로세스 모니터링 및 메모리 분석
📌 Brief Summary
Node.js는 V8 엔진 위에서 실행되며, 메모리는 주로 힙(Heap)과 스택(Stack)으로 나뉘어 관리됩니다 [1, 2]. 단일 프로세스로 오랫동안 실행되는 환경 특성상, 코드 상의 실수로 해제되지 않은 메모리 참조가 누적되면 가비지 컬렉터(GC)가 이를 회수하지 못해 Out-Of-Memory(OOM) 크래시로 이어질 수 있습니다 [2, 3]. 따라서 지속적인 메모리 사용량 모니터링과 함께, 힙 스냅샷(Heap Snapshot)과 할당 타임라인(Allocation Timeline) 등의 도구를 활용하여 누수(Leak)의 근본 원인이 되는 객체 참조를 찾아내는 분석 과정이 필수적입니다 [4-6].
📖 Core Content
-
메모리 구조 및 작동 원리
- V8 엔진의 메모리는 크게 힙(Heap)과 스택(Stack)으로 구분됩니다 [1, 2]. 스택은 원시 값(Primitive values)과 힙 객체에 대한 포인터, 그리고 함수 실행 프레임과 같은 정적 데이터를 저장하며 운영 체제에 의해 빠르게 자동으로 관리됩니다 [7-11].
- 힙은 객체와 동적 데이터가 저장되는 가장 큰 메모리 영역이며 가비지 컬렉션의 주 대상이 됩니다 [12]. 힙은 생성 주기에 따라 짧은 수명의 객체가 머무는 New Space(젊은 세대)와 오래 살아남은 객체가 승격되는 Old Space(오래된 세대) 등으로 나뉘며, 각각 Scavenge(마이너 GC)와 Mark-Sweep-Compact(메이저 GC) 알고리즘으로 관리됩니다 [12-14].
-
프로세스 메모리 모니터링 방법
- 코드 기반 모니터링:
process.memoryUsage()를 호출하면 프로세스의 메모리 사용량을 RSS(Resident Set Size), heapTotal(할당된 힙 총량), heapUsed(사용 중인 힙), external(C++ 바인딩 등 외부 메모리) 등으로 상세히 확인할 수 있습니다 [15]. - 프로덕션 환경 모니터링:
prom-client를 이용해 메모리 메트릭을 Prometheus에 내보내고, Grafana 알림을 설정하여 프로세스가 OOM으로 종료되기 전에 선제적으로 누수를 포착할 수 있습니다 [16]. - GC 활동 추적:
--trace-gc플래그를 사용하거나perf_hooks의 PerformanceObserver를 사용하여 가비지 컬렉션 로그를 확인할 수 있습니다 [17, 18]. 정상적인 프로세스는 트래픽이 몰릴 때 힙이 증가했다가 GC 이후 다시 기준선으로 돌아오는 '톱니바퀴(sawtooth)' 패턴을 보이지만, 메모리 누수가 발생하면 기준선이 계속 상승하는 '래칫(ratchet)' 패턴이 관찰됩니다 [4, 19].
- 코드 기반 모니터링:
-
메모리 분석 및 누수 탐지 도구
- 할당 타임라인 (Allocation Timeline): Chrome DevTools에서 일정 시간 동안의 할당을 기록할 수 있습니다 [20, 21]. 분석 화면에서 해제되지 않고 메모리에 여전히 남아있는 객체는 파란색 막대로 표시되며, 이 파란색 막대를 조사하여 누수 후보를 찾아낼 수 있습니다 [22-24].
- 힙 스냅샷 (Heap Snapshot): 프로세스의 메모리 상태를 포착하며, 두 개 이상의 스냅샷을 찍고 "비교(Comparison)" 뷰를 이용해 두 시점 사이에 생성되었으나 삭제되지 않은 객체들을 필터링할 수 있습니다 [25, 26]. 이 뷰를 통해 해당 객체 자체가 차지하는 얕은 크기(Shallow size)와, 객체 삭제 시 회수 가능한 보존 크기(Retained size)를 파악하고, Retainers 트리를 추적해 메모리를 붙잡고 있는 루트 원인을 찾아냅니다 [27, 28].
- 프로덕션 서버와 같이 UI가 없는 환경에서는
heapdump패키지를 통해 스냅샷을 생성하거나, V8 네이티브 프로파일링 기능인--heap-prof플래그를 사용하여 npm 패키지 의존성 없이 할당 내역을 파일로 저장하여 분석할 수 있습니다 [16, 29].
-
주요 메모리 누수 패턴 (Memory Leak Patterns)
- 이벤트 리스너 누적: 요청 핸들러 내에서
on('event', fn)과 같은 리스너를 지속적으로 추가하고 제거하지 않는 경우 발생합니다. (Node.js에서 단일 에미터에 10개 이상의 리스너가 등록될 때 발생하는MaxListenersExceededWarning은 프로덕션 누수를 확정하는 주요 신호입니다) [29]. - 클로저 변수 유지 (Closure Variable Retention): 여러 클로저가 스코프를 공유할 때, 의도치 않게 대용량 데이터(예: 전체 요청/응답 객체)가 캡처된 채 요청 수명주기를 초과해 유지되면서 발생합니다 [30, 31].
- 그 외 무제한 캐시(Unbounded Cache) 증가,
clearInterval이 누락된 타이머 인터벌, 복잡한 순환 참조(Circular References),destroy()나cancel()호출 없이 방치된 스트림(Stream)과 소켓 등이 대표적인 원인입니다 [31, 32].
- 이벤트 리스너 누적: 요청 핸들러 내에서
-
명령줄 플래그를 활용한 메모리 튜닝 (Memory Tuning)
--max-old-space-size: 장기 생존 객체가 저장되는 Old Space의 최대 크기를 메가바이트 단위로 설정하여 무거운 작업이나 세션 데이터 저장을 최적화합니다 [33].--max-semi-space-size: New Space의 크기를 조절하여 단기 객체의 잦은 생성으로 인한 마이너 GC 발생 빈도를 늦출 수 있습니다 [34].--expose-gc: 애플리케이션 코드 내에서global.gc()를 호출해 개발자가 수동으로 가비지 컬렉션을 트리거할 수 있게 합니다 [35, 36].
🔗 Knowledge Connections
- Related Topics: V8 가비지 컬렉션(Garbage Collection), Heap Snapshot, Memory Leak Patterns
- Projects/Contexts: Node.js Production Environment, Chrome DevTools Memory Panel
- Contradictions/Notes:
--expose-gc플래그를 사용하여 수동으로 GC를 실행(global.gc())할 수 있지만, 이것이 V8의 일반적인 자동 GC 알고리즘을 비활성화하는 것은 아닙니다. 수동 호출은 보조적인 역할일 뿐이며, 과도하게 사용할 경우 오히려 애플리케이션 성능에 심각한 악영향을 미칠 수 있습니다 [36].
Last updated: 2026-04-19