[2026] 메모리 누수 찾기: 프로파일러 선택과 재현 시나리오 | Valgrind·ASan·Heaptrack
이 글의 핵심
메모리 누수 프로파일링 방법: C++·Python·JavaScript별 도구 선택, 재현 시나리오 작성, 힙 분석과 의심 패턴까지 실무 순서로 정리합니다.
들어가며
메모리 누수 프로파일링 방법은 “도구를 한 번 돌린다”로 끝나지 않습니다. 재현 가능한 입력·실행 시간·부하가 없으면 프로파일러도 추측만 늘립니다. 이 글은 C++ / Python / JavaScript에서 각각 어떤 도구가 적합한지, 재현 시나리오를 어떻게 적는지, 결과를 어떻게 읽는지 순서대로 정리합니다. 2026년 기준으로도 네이티브 코드는 ASan/LSan + Valgrind/Heaptrack, 스크립트는 트레이스·힙 스냅샷 조합이 실무의 중심입니다. 운영 이슈는 C++ 메모리 누수 사례와 연결해 보면 흐름이 잡힙니다.
이 글을 읽으면
- 언어별로 프로파일러를 빠르게 고릅니다
- 재현 시나리오(스크립트·환경 변수·시드)를 문서화하는 법을 익힙니다
- 힙 그로스·리텐션 그래프를 보고 의심 지점을 좁힙니다
실무에서 마주한 현실
개발을 배울 때는 모든 게 깔끔하고 이론적입니다. 하지만 실무는 다릅니다. 레거시 코드와 씨름하고, 급한 일정에 쫓기고, 예상치 못한 버그와 마주합니다. 이 글에서 다루는 내용도 처음엔 이론으로 배웠지만, 실제 프로젝트에 적용하면서 “아, 이래서 이렇게 설계하는구나” 하고 깨달은 것들입니다. 특히 기억에 남는 건 첫 프로젝트에서 겪은 시행착오입니다. 책에서 배운 대로 했는데 왜 안 되는지 몰라 며칠을 헤맸죠. 결국 선배 개발자의 코드 리뷰를 통해 문제를 발견했고, 그 과정에서 많은 걸 배웠습니다. 이 글에서는 이론뿐 아니라 실전에서 마주칠 수 있는 함정들과 해결 방법을 함께 다루겠습니다.
목차
개념 설명
메모리 누수 유형
| 유형 | 설명 | 전형적 언어 |
|---|---|---|
| 진짜 누수 | 할당한 블록에 도달 가능한 포인터가 영구히 사라짐 | C/C++ |
| 의도치 않은 누적 | 캐시·전역 맵·이벤트 리스너가 해제되지 않음 | JS, Python, Java |
| 순환 참조 | 객체들이 서로 참조해 GC가 회수 못함 | Python (GC 전), JS (WeakMap 전) |
| 단편화 | RSS는 올라가지만 실제 사용 메모리는 안정적 | 모든 언어 |
메모리 누수 vs 단편화
아래 코드는 code를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
진짜 누수:
시간 →
메모리 ↑ ┌──────────────────── (계속 증가, 해제 안 됨)
│
└─────────────────────→
단편화:
시간 →
메모리 ↑ ┌─┐ ┌─┐ ┌─┐
│ │ │ │ │ │ (할당/해제 반복, RSS는 높지만 안정)
└─┘ └─┘ └─┘
└─────────────────────→
구분 방법:
- 진짜 누수: 힙 스냅샷에서 할당 수·크기가 계속 증가
- 단편화: 할당 수는 안정, RSS만 증가
실전 구현
C++: AddressSanitizer (ASan) + LeakSanitizer (LSan)
빌드 및 실행
아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# Clang/GCC 빌드
clang++ -fsanitize=address -g -O1 main.cpp -o app
# 실행 (누수 검사 활성화)
ASAN_OPTIONS=detect_leaks=1 ./app
# 더 자세한 출력
ASAN_OPTIONS=detect_leaks=1:log_path=asan.log:verbosity=1 ./app
예제 코드
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <iostream>
#include <vector>
void leak_example() {
int* p = new int[100]; // 누수!
// delete[] p; 누락
}
void no_leak_example() {
std::vector<int> v(100); // RAII, 자동 해제
}
int main() {
leak_example();
no_leak_example();
return 0;
}
ASan 출력
아래 코드는 code를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
=================================================================
==12345==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 400 byte(s) in 1 object(s) allocated from:
#0 0x7f8b9c in operator new[](unsigned long) (/usr/lib/x86_64-linux-gnu/libasan.so.5+0x10c9c)
#1 0x4012a3 in leak_example() /path/to/main.cpp:5
#2 0x401345 in main /path/to/main.cpp:13
#3 0x7f8b8d in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x270b3)
SUMMARY: AddressSanitizer: 400 byte(s) leaked in 1 allocation(s).
분석:
main.cpp:5에서new int[100](400바이트) 할당delete[]누락으로 누수 발생
C++: Valgrind (memcheck)
실행
아래 코드는 bash를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
# 기본 누수 검사
valgrind --leak-check=full ./app
# 모든 누수 종류 표시
valgrind --leak-check=full --show-leak-kinds=all ./app
# 입력 파일 사용
valgrind --leak-check=full ./app < input.txt
# 로그 파일 저장
valgrind --leak-check=full --log-file=valgrind.log ./app
예제 코드
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <cstdlib>
#include <cstring>
void definitely_lost() {
char* p = (char*)malloc(100);
// free(p); 누락
}
void possibly_lost() {
char* p = (char*)malloc(100);
p += 10; // 포인터 이동
// free(p - 10); 필요
}
void still_reachable() {
static char* global = (char*)malloc(100);
// 프로그램 종료까지 도달 가능
}
int main() {
definitely_lost();
possibly_lost();
still_reachable();
return 0;
}
Valgrind 출력
다음은 code를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
==12345== HEAP SUMMARY:
==12345== in use at exit: 300 bytes in 3 blocks
==12345== total heap usage: 3 allocs, 0 frees, 300 bytes allocated
==12345==
==12345== 100 bytes in 1 blocks are definitely lost in loss record 1 of 3
==12345== at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==12345== by 0x108654: definitely_lost() (main.cpp:5)
==12345== by 0x1086A3: main (main.cpp:20)
==12345==
==12345== 100 bytes in 1 blocks are possibly lost in loss record 2 of 3
==12345== at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==12345== by 0x108670: possibly_lost() (main.cpp:10)
==12345== by 0x1086A8: main (main.cpp:21)
==12345==
==12345== 100 bytes in 1 blocks are still reachable in loss record 3 of 3
==12345== at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==12345== by 0x10868C: still_reachable() (main.cpp:16)
==12345== by 0x1086AD: main (main.cpp:22)
==12345==
==12345== LEAK SUMMARY:
==12345== definitely lost: 100 bytes in 1 blocks
==12345== indirectly lost: 0 bytes in 0 blocks
==12345== possibly lost: 100 bytes in 1 blocks
==12345== still reachable: 100 bytes in 1 blocks
==12345== suppressed: 0 bytes in 0 blocks
분석:
- definitely lost: 확실한 누수 (우선 수정)
- possibly lost: 포인터 이동으로 의심
- still reachable: 전역 변수 등 (보통 무시)
C++: Heaptrack
실행
아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# 프로파일링 시작
heaptrack ./app
# GUI로 분석
heaptrack_gui heaptrack.app.12345.gz
# 텍스트 출력
heaptrack_print heaptrack.app.12345.gz
예제 코드
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <vector>
#include <string>
#include <unordered_map>
std::unordered_map<int, std::string> cache;
void process_item(int id) {
// 캐시에 계속 추가 (해제 안 됨)
cache[id] = std::string(1000, 'x');
}
int main() {
for (int i = 0; i < 100000; i++) {
process_item(i);
}
return 0;
}
Heaptrack 출력 (텍스트)
아래 코드는 code를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
MOST CALLS TO ALLOCATION FUNCTIONS
calls peak leaked function
100000 95.4M 95.4M std::string::_M_create(unsigned long&, unsigned long)
at /usr/include/c++/11/bits/basic_string.tcc:144
at process_item(int) at main.cpp:8
at main at main.cpp:13
분석:
process_item에서std::string할당이 100,000번- 총 95.4MB 누수
cache가 해제되지 않음
Python: tracemalloc
기본 사용
다음은 python를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
import tracemalloc
# 프로파일링 시작 (스택 깊이 10)
tracemalloc.start(10)
def leak_example():
global cache
cache = []
for i in range(100000):
cache.append([0] * 1000) # 누수
def snapshot_diff():
snapshot1 = tracemalloc.take_snapshot()
leak_example()
snapshot2 = tracemalloc.take_snapshot()
# 차이 분석
top_stats = snapshot2.compare_to(snapshot1, 'lineno')
print("[ Top 10 differences ]")
for stat in top_stats[:10]:
print(stat)
if __name__ == '__main__':
snapshot_diff()
출력
[ Top 10 differences ]
main.py:8: size=381 MiB (+381 MiB), count=100000 (+100000), average=4 KiB
main.py:7: size=781 KiB (+781 KiB), count=1 (+1), average=781 KiB
분석:
main.py:8(리스트 할당)에서 381MB 증가main.py:7(cache 리스트)에서 781KB 증가
고급: objgraph로 참조 추적
다음은 python를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
import objgraph
import gc
class Node:
def __init__(self, value):
self.value = value
self.next = None
def create_cycle():
a = Node(1)
b = Node(2)
a.next = b
b.next = a # 순환 참조
# 누수 생성
for _ in range(1000):
create_cycle()
# GC 실행
gc.collect()
# 가장 많은 객체 타입
objgraph.show_most_common_types(limit=10)
# 특정 객체로의 참조 경로
node_instances = objgraph.by_type('Node')
if node_instances:
objgraph.show_backrefs(node_instances[0], max_depth=5, filename='refs.png')
출력
다음은 간단한 code 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
Node 2000
dict 1523
list 892
...
분석:
Node객체가 2000개 (예상: 순환 참조로 GC 회수 안 됨)refs.png에서 참조 경로 확인
JavaScript (Node.js): 힙 스냅샷
기본 사용
다음은 javascript를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// app.js
const v8 = require('v8');
const fs = require('fs');
let cache = [];
function leakExample() {
for (let i = 0; i < 100000; i++) {
cache.push(new Array(1000).fill(0));
}
}
function takeSnapshot(tag) {
if (global.gc) global.gc(); // 강제 GC
const filename = `heap-${tag}-${Date.now()}.heapsnapshot`;
v8.writeHeapSnapshot(filename);
console.log(`Snapshot saved: ${filename}`);
}
// 스냅샷 1
takeSnapshot('before');
// 누수 발생
leakExample();
// 스냅샷 2
takeSnapshot('after');
console.log('Memory usage:', process.memoryUsage());
실행
# --expose-gc로 global.gc 활성화
node --expose-gc app.js
Chrome DevTools 분석
- Chrome DevTools 열기 (
chrome://inspect) - Memory 탭 → Load 버튼
heap-before-*.heapsnapshot와heap-after-*.heapsnapshot로드- Comparison 뷰에서 차이 확인 분석 예시:
Constructor | # New | # Deleted | # Delta | Size Delta
(array) | 100000| 0 | +100000 | +381 MB
(string) | 523 | 12 | +511 | +2.3 MB
(array)객체가 100,000개 증가- 381MB 증가 →
cache배열이 원인
고급: —inspect로 실시간 프로파일링
아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# 디버그 모드로 실행
node --inspect app.js
# 또는 중단점 대기
node --inspect-brk app.js
- Chrome에서
chrome://inspect접속 - Open dedicated DevTools for Node 클릭
- Memory 탭에서 실시간 힙 스냅샷 촬영
Python: memory_profiler
설치 및 사용
pip install memory_profiler
아래 코드는 python를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
from memory_profiler import profile
@profile
def leak_example():
cache = []
for i in range(100000):
cache.append([0] * 1000)
return cache
if __name__ == '__main__':
result = leak_example()
실행
python -m memory_profiler app.py
출력
아래 코드는 code를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
Line # Mem usage Increment Occurrences Line Contents
=============================================================
3 38.5 MiB 38.5 MiB 1 @profile
4 def leak_example():
5 38.5 MiB 0.0 MiB 1 cache = []
6 419.9 MiB 381.4 MiB 100001 for i in range(100000):
7 419.9 MiB 381.4 MiB 100000 cache.append([0] * 1000)
8 419.9 MiB 0.0 MiB 1 return cache
분석:
- 6-7번 줄에서 381.4MB 증가
cache.append가 원인
고급 활용
1) 재현 시나리오 문서화
메모리 누수는 재현 가능한 입력이 없으면 디버깅이 불가능합니다.
재현 스크립트 예시
다음은 bash를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#!/bin/bash
# reproduce_leak.sh
# 환경 변수 설정
export ASAN_OPTIONS=detect_leaks=1:log_path=asan.log
export MALLOC_CONF=prof:true,lg_prof_interval:30
# 입력 데이터 생성
python generate_input.py --size 10000 --seed 42 > input.txt
# 애플리케이션 실행
./app < input.txt
# 결과 확인
if grep -q "LeakSanitizer" asan.log.*; then
echo "Memory leak detected!"
exit 1
else
echo "No leak detected"
exit 0
fi
재현 조건 문서
다음은 markdown를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
# 메모리 누수 재현 조건
## 환경
- OS: Ubuntu 22.04
- 컴파일러: GCC 11.3
- 빌드 플래그: `-fsanitize=address -g -O1`
## 재현 단계
1. `generate_input.py --size 10000 --seed 42` 실행
2. `ASAN_OPTIONS=detect_leaks=1 ./app < input.txt` 실행
3. 약 30초 후 누수 보고 확인
## 예상 결과
- 누수 크기: ~400 bytes
- 누수 위치: `main.cpp:5` (leak_example 함수)
## 재현율
- 10회 중 10회 재현 (100%)
2) 장기 실행 누수 탐지
부하 테스트 스크립트
다음은 bash를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#!/bin/bash
# load_test.sh
# 1시간 동안 요청 반복
for i in {1..3600}; do
curl http://localhost:8080/api/process
sleep 1
# 10분마다 메모리 사용량 기록
if [ $((i % 600)) -eq 0 ]; then
ps aux | grep app | awk '{print $6}' >> memory.log
fi
done
# 메모리 증가 추세 분석
python analyze_memory.py memory.log
메모리 분석 스크립트
다음은 python를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
# analyze_memory.py
import sys
import matplotlib.pyplot as plt
def analyze_memory(log_file):
with open(log_file) as f:
memory_kb = [int(line.strip()) for line in f]
# 메모리 증가율 계산
if len(memory_kb) < 2:
print("Not enough data")
return
initial = memory_kb[0]
final = memory_kb[-1]
growth_rate = (final - initial) / initial * 100
print(f"Initial memory: {initial / 1024:.2f} MB")
print(f"Final memory: {final / 1024:.2f} MB")
print(f"Growth rate: {growth_rate:.2f}%")
# 그래프 생성
plt.plot(memory_kb)
plt.xlabel('Time (10 min intervals)')
plt.ylabel('Memory (KB)')
plt.title('Memory Usage Over Time')
plt.savefig('memory_trend.png')
print("Graph saved: memory_trend.png")
if __name__ == '__main__':
analyze_memory(sys.argv[1])
3) 프로덕션 환경 모니터링
Prometheus + Grafana
다음은 python를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
# Python 예시 (prometheus_client)
from prometheus_client import Gauge, start_http_server
import psutil
import time
# 메트릭 정의
memory_usage = Gauge('app_memory_bytes', 'Application memory usage in bytes')
heap_size = Gauge('app_heap_bytes', 'Heap size in bytes')
def collect_metrics():
process = psutil.Process()
while True:
# RSS 메모리
memory_usage.set(process.memory_info().rss)
# Python 힙 크기
import sys
heap_size.set(sys.getsizeof(globals()))
time.sleep(10)
if __name__ == '__main__':
start_http_server(8000)
collect_metrics()
Grafana 쿼리
아래 코드는 promql를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# 메모리 증가율 (1시간 기준)
rate(app_memory_bytes[1h])
# 메모리 사용량 추세
app_memory_bytes
# 임계값 초과 알림
app_memory_bytes > 1e9 # 1GB 초과
성능과 비교
| 도구 | 언어 | 오버헤드 | 정확도 | 사용 시점 |
|---|---|---|---|---|
| ASan/LSan | C/C++ | 2x | 높음 | 개발·CI |
| Valgrind | C/C++ | 10-50x | 매우 높음 | 심층 분석 |
| Heaptrack | C/C++ | 1.5x | 높음 | 할당 핫스팟 |
| tracemalloc | Python | 낮음 | 높음 | 개발·프로덕션 |
| objgraph | Python | 낮음 | 높음 | 순환 참조 |
| 힙 스냅샷 | Node.js | 낮음 | 높음 | 개발·프로덕션 |
| —inspect | Node.js | 낮음 | 높음 | 실시간 디버깅 |
도구 선택 플로우차트
아래 코드는 code를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
언어가 무엇인가?
├─ C/C++
│ ├─ 빠른 검증? → ASan/LSan
│ ├─ 미정의 동작 포함? → Valgrind
│ └─ 할당 핫스팟? → Heaptrack
├─ Python
│ ├─ 할당 추적? → tracemalloc
│ └─ 순환 참조? → objgraph
└─ JavaScript (Node.js)
├─ 개발 환경? → 힙 스냅샷
└─ 프로덕션? → --inspect + 원격 디버깅
실무 사례
사례 1: C++ 웹 서버 - shared_ptr 순환 참조
증상: 연결 종료 후에도 메모리 증가
문제 코드
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class Connection {
public:
std::shared_ptr<Session> session;
};
class Session {
public:
std::shared_ptr<Connection> connection; // 순환 참조!
};
void handle_connection() {
auto conn = std::make_shared<Connection>();
auto sess = std::make_shared<Session>();
conn->session = sess;
sess->connection = conn; // 순환 참조 발생
// 함수 종료 시 둘 다 해제 안 됨 (참조 카운트 1 유지)
}
Heaptrack 출력
다음은 간단한 code 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
MOST CALLS TO ALLOCATION FUNCTIONS
calls peak leaked function
10000 960K 960K std::make_shared<Connection>
10000 1.2M 1.2M std::make_shared<Session>
해결
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class Connection {
public:
std::shared_ptr<Session> session;
};
class Session {
public:
std::weak_ptr<Connection> connection; // weak_ptr로 변경
};
void handle_connection() {
auto conn = std::make_shared<Connection>();
auto sess = std::make_shared<Session>();
conn->session = sess;
sess->connection = conn; // weak_ptr는 참조 카운트 증가 안 함
// 함수 종료 시 정상 해제
}
사례 2: Node.js 서버 - 이벤트 리스너 누수
증상: 요청 처리 후 메모리 증가
문제 코드
다음은 javascript를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
const EventEmitter = require('events');
const emitter = new EventEmitter();
function handleRequest(req, res) {
const handler = (data) => {
res.write(data);
};
// 리스너 등록
emitter.on('data', handler);
// 응답 전송
res.end('OK');
// 리스너 해제 누락!
}
// 10,000번 요청 → 10,000개 리스너 누적
힙 스냅샷 분석
다음은 간단한 code 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
Constructor | Retained Size | Distance
(closure) | 381 MB | 3
context | 381 MB | 4
handler | 381 MB | 5
분석:
handler클로저가 381MB 유지res객체를 계속 참조
해결
아래 코드는 javascript를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
function handleRequest(req, res) {
const handler = (data) => {
res.write(data);
};
emitter.on('data', handler);
res.on('finish', () => {
emitter.off('data', handler); // 리스너 해제
});
res.end('OK');
}
사례 3: Python 웹 앱 - 전역 캐시 누수
증상: 장기 실행 시 메모리 계속 증가
문제 코드
아래 코드는 python를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
# 전역 캐시 (해제 안 됨)
cache = {}
def process_request(user_id):
if user_id not in cache:
# 대용량 데이터 로드
cache[user_id] = load_user_data(user_id)
return cache[user_id]
# 사용자 수 증가 → 캐시 무한 증가
tracemalloc 출력
app.py:6: size=2.3 GiB (+2.3 GiB), count=50000 (+50000), average=48 KiB
해결: LRU 캐시
아래 코드는 python를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
from functools import lru_cache
@lru_cache(maxsize=1000) # 최대 1000개 유지
def get_user_data(user_id):
return load_user_data(user_id)
def process_request(user_id):
return get_user_data(user_id)
개선 효과:
- 메모리: 무제한 → 최대 48MB (1000 × 48KB)
- 오래된 항목 자동 제거
트러블슈팅
문제 1: “로컬에선 안 나는데 서버만 증가한다”
원인
- 스레드 수·풀 크기·TLS 버퍼 차이
- 부하 패턴 차이 (로컬: 단일 요청, 서버: 동시 1000개)
해결
아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# 서버와 동일한 부하 생성
wrk -t12 -c400 -d30s http://localhost:8080/api/process
# 또는 Apache Bench
ab -n 10000 -c 100 http://localhost:8080/api/process
# 메모리 모니터링
watch -n 1 'ps aux | grep app | awk "{print \$6}"'
문제 2: “Valgrind가 너무 느리다”
원인
- Valgrind는 10-50배 느림
- 큰 입력으로 실행 시 수 시간 소요
해결: 최소 재현 케이스
아래 코드는 bash를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
# 이진 탐색으로 입력 크기 줄이기
# 1. 입력 절반으로 줄이기
head -n 5000 input.txt > input_half.txt
valgrind --leak-check=full ./app < input_half.txt
# 2. 누수 재현되면 계속 줄이기
head -n 2500 input_half.txt > input_quarter.txt
valgrind --leak-check=full ./app < input_quarter.txt
# 3. 최소 재현 케이스 확보
# 예: 100줄로 재현 가능 → Valgrind 실행 시간 대폭 단축
문제 3: “Python이 GC라서 괜찮다?”
오해
아래 코드는 python를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# GC가 있어도 누수 가능
cache = {} # 전역 변수
def add_to_cache(key, value):
cache[key] = value # GC가 회수 못함 (전역에서 참조)
해결
아래 코드는 python를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
import weakref
# WeakValueDictionary 사용
cache = weakref.WeakValueDictionary()
def add_to_cache(key, value):
cache[key] = value # 다른 참조 없으면 GC 회수 가능
문제 4: “ASan 빌드가 프로덕션에서 안 돌아간다”
원인
- ASan은 2배 메모리 오버헤드
- 프로덕션 배포 불가
해결: 스테이징 환경
다음은 yaml를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
# CI/CD 파이프라인
stages:
- build
- test
- staging
- production
staging:
script:
- clang++ -fsanitize=address -g main.cpp -o app_asan
- ASAN_OPTIONS=detect_leaks=1 ./app_asan
- if grep -q "LeakSanitizer" asan.log.*; then exit 1; fi
production:
script:
- clang++ -O3 main.cpp -o app # ASan 없음
- ./deploy.sh
마무리
메모리 누수 프로파일링 방법의 공통분모는 재현 시나리오와 할당 관점(스택·호출 경로)입니다. C++ 사례 심화는 메모리 누수 디버깅 실전 사례와 함께 보시면 도구 출력을 실제 수정까지 연결하기 쉽습니다.
핵심 요약
- 도구 선택
- C++: ASan (빠름) → Valgrind (정확) → Heaptrack (할당 분석)
- Python: tracemalloc → objgraph (순환 참조)
- Node.js: 힙 스냅샷 → —inspect (실시간)
- 재현 시나리오
- 입력 데이터 고정 (시드 사용)
- 환경 변수 문서화
- 부하 패턴 재현 (wrk, ab)
- 분석 방법
- 할당 스택 트레이스 확인
- 시간에 따른 메모리 추세 그래프
- 힙 스냅샷 비교 (before/after)
- 일반적 원인
- C++:
delete누락, 순환 참조 (shared_ptr) - Python: 전역 캐시, 순환 참조
- Node.js: 이벤트 리스너 미해제, 클로저 누수
- C++:
다음 단계
- C++ 심화: 메모리 누수 디버깅 실전 사례
- 성능 최적화: C++ 성능 최적화 사례
- Python 프로파일링: Python 성능 최적화 메모리 누수는 재현만 되면 절반은 해결입니다. 재현 스크립트를 먼저 만들고, 프로파일러 출력을 차근차근 읽어 나가세요.