Node.js의 프로세스 메모리 관리 방식
Node.js에서는 프로세스의 메모리를 어떻게 관리할까?
Node.jsNode.js는 Google의 V8 JavaScript 엔진을 기반으로 해 메모리를 관리한다.
실행 중인 Node.js 프로그램은 Resident Set이라는 메모리 공간에서 표현되며, 이는 다음과 같이 구성된다.
- Code: 실제 실행되는 코드
- Stack: 정적 데이터(함수 프레임, 원시값, 힙 객체에 대한 포인터 등)
- Heap: 객체나 동적 데이터가 저장되는 가장 큰 메모리 영역
즉, 데이터 타입에 따라서 자동으로 스택이나 힙에 할당된다.
Node.js의 메모리 할당 방식
1. 힙 메모리 구조
V8의 세대별 가설을 기반으로 메모리를 관리한다. 이는 대부분의 객체가 짧은 생명주기를 가진다는 가정 하에 힙을 두 개의 세대로 나눈다.
New Space (새로운 공간)
- 새로 생성된 단명 객체들이 할당되는 공간
- e.g.) API 요청마다 생성되는 임시 객체
- 빈번한 가비지 컬렉션이 발생하여 메모리를 빠르게 회수
Old Space (오래된 공간)
- New Space에서 여러 가비지 컬렉션 주기를 살아남은 객체들이 승격되는 공간
- e.g.) 사용자 세션 데이터
- 가비지 컬렉션이 덜 빈번하지만 더 많은 리소스를 소모
2. 스택 메모리
스택은 지역변수와 함수 호출 정보를 저장하는 LIFO 구조다. 힙과 달리 가비지 컬렉터가 관리하지 않으며, 함수가 반환될 때 자동으로 해제된다.
Node.js의 가비지 컬렉션 메커니즘
가비지 컬렉션: 메모리 관리 방법 중 하나, 동적으로 할당한 메모리 영역 중 더 이상 쓰이지 않는 영역을 자동으로 찾아내어 해제
1. Scavenger (Minor GC)
- New Space의 단명 객체들을 대상으로 하는 알고리즘
- 사용되지 않는 객체는 메모리에서 제거, 여전히 사용중이면 Old Space로 이동
2. Mark-Sweep & Mark-Compact
- Old Space의 장수명 객체들을 대상으로 하는 알고리즘
- 사용 중인 객체를 마킹하고, 마킹되지 않은 객체를 제거
메모리 사용량 모니터링
console.log(process.memoryUsage());
위 메소드를 통해 메모리 사용량을 실시간으로 확인한다.
반환되는 정보
- rss: Resident Set Size, 프로세스가 사용하는 전체 메모리
- heapTotal: V8 힙의 총 크기
- heapUsed: V8 힙에서 실제로 사용 중인 메모리
- external: V8가 관리하지 않는 C++ 객체의 메모리 사용량
- arrayBuffers: ArrayBuffer 객체가 사용하는 메모리
메모리 최적화 방법
1. 명령줄 플래그 활용
--max-old-space-size: Old Space의 최대 크기를 설정--max-semi-space-size: New Space의 최대 크기를 설정
2. 메모리 누수 방지
- 전역 객체 사용 최소화
- 이벤트 리스너 적절한 해제
- 순환 참조 피하기
일반적인 메모리 모델과의 차이점
1. 메모리 생명 주기
| 일반 메모리 | Node.js |
|---|---|
| 필요한 메모리 할당 | JS는 메모리 할당과 해제를 자동으로 처리 |
| 할당된 메모리 사용(읽기, 쓰기) | 개발자가 명시적으로 메모리를 관리할 필요가 없음 |
| 더 이상 필요하지 않을 때 메모리 해제 | 가비지 컬렉션을 통한 자동 메모리 정리 |
2. 메모리 할당 방식
| 일반 메모리 | Node.js |
|---|---|
| 스택과 힙으로 구분 | V8 엔진의 세대별 힙 구조 사용 |
| 스택: 정적 데이터(문자열 값, 객체 주소) | New Space와 Old Space로 세분화된 힙 관리 |
| 힙: 동적 데이터(객체 포함) | 객체의 생명주기에 따른 효율적 메모리 관리 |
3. 가비지 컬렉션 알고리즘
| 일반 메모리 | Node.js |
|---|---|
| 참조 카운팅 방식: 참조 횟수가 0이 되면 해제 | Mark-and-Sweep 알고리즘 사용 |
| 루트에서 도달 가능한지 여부로 판단 | |
| 순환 참조 문제 발생 가능 | 순환 참조 문제 해결 |