5.5 KiB
5.5 KiB
Node.js Memory Management
📌 Brief 정Summary
Node.js 메모리 관리는 구글의 V8 자바스크립트 엔진에 의해 수행되며, 스택(Stack)과 힙(Heap) 메모리 구조를 기반으로 데이터를 관리합니다 [1-3]. V8은 '대부분의 객체는 일찍 죽는다(Generational Hypothesis)'는 가설에 기반한 세대별 가비지 컬렉션(Generational Garbage Collection)을 사용하여 자동으로 사용되지 않는 메모리를 회수합니다 [4-6]. 단일 프로세스로 장시간 실행되는 Node.js의 특성상 참조가 유지된 채 힙에 쌓이는 객체들은 메모리 누수와 OOM(Out of Memory) 충돌의 주원인이 되므로, 메모리 할당 패턴을 이해하고 적절한 튜닝과 디버깅을 수행하는 것이 필수적입니다 [7-9].
📖 Core Content
V8 메모리 아키텍처 (Stack & Heap)
- Node.js를 실행하는 V8 엔진은 메모리를 스택(Stack)과 힙(Heap)으로 나누어 관리합니다 [1, 2].
- 스택은 정적 데이터, 메서드/함수 프레임, 원시 값, 힙에 있는 객체를 가리키는 포인터를 LIFO(Last In, First Out) 방식으로 저장하며, 운영체제에 의해 매우 빠르게 자동 관리됩니다 [1, 10-12].
- 힙은 실행 시간에 크기가 결정되는 동적 객체가 저장되는 곳으로 가비지 컬렉터(GC)의 주요 관리 대상이 됩니다 [10, 13, 14]. 힙 내부는 객체의 수명과 목적에 따라 New-space(Young generation), Old-space(Old generation), Large-object-space, Code-space, Map-space 등으로 세분화됩니다 [14-16].
세대별 가비지 컬렉션 (Generational Garbage Collection)
- Minor GC (Scavenger): New-space를 관리하는 빠르고 빈번한 컬렉터입니다 [17, 18]. New-space는 절반씩 To-space와 From-space로 나뉘며(Cheney's algorithm), 할당 포인터가 공간 끝에 도달하면 살아있는 객체만 To-space로 복사하고 죽은 객체를 버립니다 [18-20]. 이 과정을 두 번 생존한 객체는 Old-space로 승격(Promotion)됩니다 [17, 19, 20].
- Major GC (Mark-Sweep-Compact): Old-space가 일정 한도에 도달하면 실행되며, Mark-Sweep-Compact 알고리즘을 사용합니다 [17, 21-23]. 루트(스택, 전역 객체 등)에서 시작해 도달 가능한 객체를 탐색하여 마킹(Marking)하고, 도달할 수 없는 영역을 회수(Sweeping)하며, 필요 시 살아남은 객체를 모아 단편화를 줄이는 압축(Compacting)을 수행합니다 [21, 24-27].
- Orinoco 프로젝트: 전통적인 GC의 단점인 긴 일시 정지(Stop-the-world) 문제를 해결하기 위해 도입된 V8의 GC 아키텍처입니다 [28-30]. 작업 스레드를 활용하여 GC 작업을 병렬(Parallel), 점진적(Incremental), 동시적(Concurrent)으로 수행하여 메인 스레드의 부하와 지연을 최소화합니다 [31-37].
메모리 누수 (Memory Leaks) 발생 패턴 및 분석
- Node.js에서 메모리 누수는 객체가 유실된 것이 아니라 개발자가 의도치 않게 참조(Reference)를 유지하여 가비지 컬렉터가 이를 살아있는 것으로 간주할 때 발생합니다 [8, 38, 39].
- 정상적인 GC 사이클을 거치는 애플리케이션은 톱니바퀴(Sawtooth) 형태의 메모리 사용 패턴을 보이지만, 누수가 있는 경우 해제되지 않고 계속 증가만 하는 라쳇(Ratchet) 패턴을 보입니다 [40-42].
- 주요 누수 원인으로는 이벤트 리스너 누적(예:
EventEmitter경고), 해제되지 않은 타이머/인터벌(Timer Drift), 클로저 변수 보존(Closure Retention), 한도 없는 인메모리 캐시, 종료되지 않은 스트림(Streams) 등이 있습니다 [39, 43-46].
모니터링 및 메모리 튜닝 (Monitoring and Tuning)
- 코드 상에서
process.memoryUsage()를 통해 rss, heapTotal, heapUsed 등의 메모리 지표를 추적할 수 있으며 [47, 48],--trace-gc플래그나 V8 모듈, 퍼포먼스 훅(Performance Hooks)을 통해 GC 활동 로그를 분석할 수 있습니다 [49-51]. - 발견하기 힘든 누수 분석 시에는 Chrome DevTools의 Memory 패널을 이용하여 힙 스냅샷(Heap Snapshots)을 비교하거나 Allocation Timeline을 기록하여 누수 대상을 추적합니다 [40, 52-57].
- Node.js 실행 시 플래그를 통해 메모리를 제어할 수 있습니다.
--max-old-space-size로 Old-space 한도를 늘리거나,--max-semi-space-size로 New-space 크기를 키울 수 있으며,--expose-gc를 설정하면 애플리케이션에서global.gc()를 통해 수동으로 GC를 유발할 수 있습니다 [58-62]. - 포인터 압축(Pointer Compression) 기술로 인해 64비트 시스템에서도 V8 힙은 최대 4GB로 제한될 수 있으며, 이를 초과할 경우 빈번한 GC 발생 및 OOM이 일어날 수 있습니다 [63-66].
🔗 Knowledge Connections
- Related Topics: V8 JavaScript Engine, Garbage Collection, Orinoco GC, Memory Leaks, Pointer Compression
- Projects/Contexts: Node.js Production Monitoring, Chrome DevTools Memory Profiling
- Contradictions/Notes:
--expose-gc옵션을 사용해 코드 내에서 수동으로 GC(global.gc())를 호출하여 메모리를 회수할 수는 있으나, 과도하게 사용하면 프로그램 성능 저하(Performance degradation)를 초래할 수 있으므로 주의해서 사용해야 한다고 경고합니다 [62, 67].
Last updated: 2026-04-19