· 6 min read
AI에게 메모리 프로파일링을 시켰다 I Had AI Do Memory Profiling
136MB에서 14GB로, 2분 만에. dotnet-gcdump로는 안 잡히는 할당 폭탄을 AI에게 진단시키고, 그 과정을 재사용 가능한 스킬로 만든 이야기. From 136MB to 14GB in 2 minutes. Having AI diagnose an allocation bomb invisible to dotnet-gcdump, then turning the process into a reusable skill.
문제
Aethelgard 서버의 모니터링 로그에서 이런 수치가 나왔다:
| 시각 | GC Heap | 상태 |
|---|---|---|
| 16:40:42 | 136MB | idle |
| 16:41:22 | 227MB | 여전히 idle (+91MB) |
| 16:42:22 | 1,913MB | 클라이언트 1명 접속 |
| 16:42:32 | 14,312MB | 10초 만에 +12GB |
2분 만에 136MB에서 14GB. 클라이언트가 1명만 접속해도 초당 75MB씩 할당이 폭증했다.
메모리 프로파일링은 할 줄 안다. 하지만 dotnet-gcdump 뜨고, counters 돌리고, 덤프 분석하고, 가설 세우고 검증하는 일련의 과정이 시간이 걸린다. 이번에는 AI에게 시켜보기로 했다.
AI에게 시키기
로그를 Claude Code에 던지고 이렇게 말했다:
“분석 시작해. 원인임 맞는지 검증부터 해. dotnet dump같은 거 써도 돼.”
그리고 하나 더:
“이런 경우가 앞으로도 있을 것 같아서 나중에 skill로 정리해두게.”
단발성 디버깅이 아니라, 다음에도 쓸 수 있는 프로세스를 만드는 게 목표였다.
진단 과정
AI가 알아서 진행한 과정을 요약하면 이렇다.
1. 코드 분석 → 가설
AoIBroadcastProcessor.cs에서 수상한 패턴을 찾았다:
if (!state.FilteredSyncFieldsInUse) {
filtered = state.FilteredSyncFields; // 첫 번째: 재사용
state.FilteredSyncFieldsInUse = true;
} else {
filtered = new SyncFields(); // 두 번째~: 매 프레임 new
filtered.FlatBuffer = new byte[visibleBytes];
}boolean 플래그로 재사용을 관리하고 있었다. 프레임당 13개 SyncSystem이 호출하는데, 1개만 재사용하고 12개는 매번 new. 가설은 세워졌지만 검증이 필요하다.
2. dotnet-gcdump → 함정
gcdump를 떴더니 heap이 깨끗했다. 클라이언트를 접속시켜도 마찬가지.
여기서 AI가 핵심적인 판단을 냈다:
“dotnet-gcdump는 수집 전에 GC를 강제 실행합니다. 이건 ‘릭’이 아니라 GC 압력 문제일 가능성이 높습니다.”
gcdump가 GC를 먼저 돌리니까, 단수명 객체가 다 회수돼서 안 보이는 거였다. 처음 써보면 놓치기 쉬운 함정인데, AI가 도구의 특성을 이해하고 있었다.
3. dotnet-counters → 진짜 원인
allocation rate를 실시간으로 측정했다:
| 상태 | Alloc Rate | GC 후 Heap |
|---|---|---|
| Idle (5x5 월드) | 2.4 MB/s | 복귀 |
| 클라이언트 1명 | 75 MB/s | 1.3GB → GC → 246MB → 다시 폭증 |
GC가 돌면 1GB+ 회수 — 전부 단수명 객체. 진짜 릭이 아니라, Server GC의 지연 수집 + 불필요한 대량 할당이 맞물려서 OOM으로 가는 패턴이었다.
수정
boolean → 인덱스 기반 리스트로 변경:
internal SyncFields RentFilteredSyncFields(int requiredBufferSize) {
if (FilteredSyncFieldsUsed < FilteredSyncFieldsList.Count) {
var sf = FilteredSyncFieldsList[FilteredSyncFieldsUsed++];
if (sf.FlatBuffer.Length < requiredBufferSize)
sf.FlatBuffer = new byte[requiredBufferSize];
return sf;
}
var newSf = new SyncFields { FlatBuffer = new byte[requiredBufferSize] };
FilteredSyncFieldsList.Add(newSf);
FilteredSyncFieldsUsed++;
return newSf;
}첫 프레임에서 13개를 할당하면, 두 번째 프레임부터 할당이 0이다.
프레임당 2,664 bytes → 35 bytes (99% 감소). 회귀 방지를 위해 GC.GetAllocatedBytesForCurrentThread() 기반 유닛 테스트도 작성했다.
skill로 만들기
이 세션의 진짜 산출물은 버그 수정이 아니라 /mem-profile 스킬이다.
/mem-profile baseline # counters 15초 수집 → 요약
/mem-profile snapshot idle # counters + gcdump 한 쌍
/mem-profile snapshot loaded # 부하 상태에서 수집
/mem-profile compare # 두 snapshot 비교
/mem-profile watch 60 # 실시간 추이 관찰이번에 배운 진단 패턴이 스킬 안에 들어있다:
- GC 압력 vs 진짜 릭: GC 후 heap이 복귀하면 GC 압력, 계속 증가하면 릭
- gcdump의 한계: GC 강제 실행으로 단수명 객체가 사라짐 — counters와 반드시 함께 볼 것
- Server GC 주의: heap 증가 = 릭이 아닐 수 있음
프로젝트 전용이 아니라 .NET 프로세스 범용으로 만들었다. 다음에 메모리 문제가 생기면 /mem-profile baseline부터 시작하면 된다. AI가 스킬을 읽고 같은 진단 프로세스를 자동으로 밟는다. gcdump 함정에 다시 빠지지 않는다.
정리
메모리 프로파일링 자체가 어려운 건 아니다. 하지만 도구를 골라서 돌리고, 결과를 해석하고, 가설을 세우고 검증하는 루프를 반복하는 데 시간이 든다. AI에게 시키면 이 루프가 빠르게 돈다. 그리고 그 과정을 skill로 남기면 다음번에는 더 빠르다.

