콘텐츠로 이동

메모리 누수(Memory Leak) 진단 & 해결 — JVM, Node.js, Python

메모리 누수(Memory Leak)는 조용히 쌓이다가 OOM(Out of Memory) 으로 갑자기 터집니다. Soak Test에서 시간이 지날수록 응답 시간이 증가한다면 누수를 의심하세요.

정상: 메모리 ████░░░░░░ GC 후 복구
성장형 누수: ████████████░░ GC 후 일부 복구, 전반적 증가
고착형 누수: ████████████████ 복구 없음 → OOM 불가피

공통 원인:

  • 해제되지 않는 이벤트 리스너
  • 무한히 증가하는 캐시 (만료 없는 Map, 딕셔너리)
  • DB 커넥션 / 파일 핸들 미반납
  • 글로벌 변수에 쌓이는 데이터
Terminal window
# JVM 시작 옵션에 추가
java -Xms512m -Xmx1g \
-XX:+PrintGCDetails \
-XX:+PrintGCDateStamps \
-Xloggc:/var/log/app/gc.log \
-jar app.jar
Terminal window
# GCEasy (온라인 분석) 또는 GCViewer로 시각화
# GC 후 힙이 완전히 복구되지 않는 패턴 → 누수 의심
Terminal window
# 실행 중인 JVM 힙 덤프
jmap -dump:format=b,file=heap.hprof <PID>
# OOM 발생 시 자동 덤프 (시작 옵션에 추가)
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/app/
Terminal window
# Eclipse MAT (Memory Analyzer Tool)로 분석
# → "Leak Suspects" 리포트 자동 생성
# → 가장 많은 메모리를 점유하는 객체 트리 확인
Terminal window
# 어느 코드에서 객체가 많이 생성되는지 추적
./profiler.sh -e alloc -d 30 -f alloc.html <PID>
// 누수 1: 이벤트 리스너 미제거
class DataProcessor extends EventEmitter {
process() {
// 매 호출마다 리스너가 쌓임
db.on('data', (row) => this.handle(row)); // ❌
// 올바른 방법: 한 번만 등록하거나 명시적 제거
const handler = (row) => this.handle(row);
db.on('data', handler);
db.once('end', () => db.removeListener('data', handler)); // ✅
}
}
// 누수 2: 만료 없는 캐시
const cache = new Map();
function getCached(key) {
if (!cache.has(key)) {
cache.set(key, fetchData(key)); // 영원히 쌓임 ❌
}
return cache.get(key);
}
// 해결: LRU 캐시 사용
const LRU = require('lru-cache');
const cache = new LRU({ max: 1000, ttl: 1000 * 60 * 5 }); // 최대 1000개, 5분 TTL ✅
Terminal window
# Node.js 인스펙터 활성화
node --inspect app.js
# Chrome: chrome://inspect → 대상 연결
# Memory 탭 → "Take heap snapshot"
# 일정 시간 후 다시 스냅샷
# "Comparison" 뷰에서 증가한 객체 확인
Terminal window
npm install -g clinic
# 메모리 프로파일링
clinic heapprofile -- node app.js
# 결과 HTML 자동 생성: 함수별 메모리 할당 시각화
# tracemalloc으로 메모리 할당 추적
import tracemalloc
tracemalloc.start()
# ... 코드 실행 ...
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
print("=== 메모리 상위 10개 할당 지점 ===")
for stat in top_stats[:10]:
print(stat)
# memory_profiler로 함수별 메모리 사용량
from memory_profiler import profile
@profile
def process_large_dataset():
data = load_all_records() # 메모리 급증 지점 확인
results = [transform(r) for r in data]
return results
검증 방법:
1. 초기 메모리 사용량 기록
2. 50 VU로 4~8시간 Soak Test 실행
3. 시간별 메모리 사용량 그래프 확인
판단 기준:
✅ 정상: GC 후 초기 메모리 수준 유지
⚠️ 의심: 완만하게 증가하다 일정 수준 이상에서 안정
❌ 누수: 지속적으로 증가, 복구 없음
# Prometheus로 JVM 힙 사용량 추이
jvm_memory_used_bytes{area="heap"}
# Node.js process 메모리
process_resident_memory_bytes
visitor count

이 가이드를 내 서비스에 직접 적용해 보세요.

TestForge 무료 스캔 시작 →