· 6 min read

AI에게 메모리 프로파일링을 시켰다

136MB에서 14GB로, 2분 만에. dotnet-gcdump로는 안 잡히는 할당 폭탄을 AI에게 진단시키고, 그 과정을 재사용 가능한 스킬로 만든 이야기.

문제

Aethelgard 서버의 모니터링 로그에서 이런 수치가 나왔다:

시각GC Heap상태
16:40:42136MBidle
16:41:22227MB여전히 idle (+91MB)
16:42:221,913MB클라이언트 1명 접속
16:42:3214,312MB10초 만에 +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 RateGC 후 Heap
Idle (5x5 월드)2.4 MB/s복귀
클라이언트 1명75 MB/s1.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로 남기면 다음번에는 더 빠르다.

Back to Blog

Related Posts

View All Posts »
Claude Code 구독으로 CI 파이프라인을 만들었다

Claude Code 구독으로 CI 파이프라인을 만들었다

AI와 함께 코딩하니 테스트가 늘고, 테스트가 늘으니 CI가 필요해졌다. self-hosted runner에 Claude Code 구독을 물려서 테스트 자동화와 PR 변경 분석까지 돌리는 파이프라인.

#Aethelgard #AI #Claude #GameDev