게임이 오래 켜져 있을수록 메모리가 늘어난다면 GC부터 의심하기 쉽습니다. 그런데 프레임마다 임시 객체를 많이 만드는 문제와, 쓰지 않는 객체를 계속 붙잡고 있는 문제는 다릅니다. 텍스처처럼 관리 힙 밖의 자원이 늘어나는 경우도 따로 봐야 합니다.
앱이 꺼졌다는 증상만으로 메모리 부족을 확정할 수도 없습니다. 기기 로그와 크래시 정보를 먼저 확인합니다. ‘RAM 4GB 기기에서는 앱이 2GB까지 쓸 수 있다’처럼 고정 한도를 가정하지 않습니다. OS와 기기, 다른 프로세스의 상태에 따라 조건이 달라집니다.
GC.Alloc이 늘었다고 곧 누수는 아니다
GC.Alloc은 관리 메모리 할당을 조사하는 단서입니다. 매 프레임 문자열이나 임시 컬렉션을 만들면 할당이 반복될 수 있습니다. 하지만 해당 객체가 더 이상 참조되지 않아 수거된다면, 이것만으로 계속 누적되는 누수라고 부를 수는 없습니다.
반대로 매 프레임 할당은 적어도 static 컬렉션이나 이벤트 구독이 객체를 계속 참조하면 오래 살아남을 수 있습니다. 한 번 만든 큰 텍스처를 해제하지 않는 문제도 ‘매 프레임 할당 0’만으로는 찾지 못합니다.
| 보이는 증상 | 먼저 구분할 것 |
|---|---|
| 주기적으로 프레임이 끊김 | 해당 구간의 GC와 다른 CPU 작업 |
| 팝업을 반복해서 열 때 객체 수 증가 | 캐시·풀의 의도된 증가인지 남은 참조인지 |
| 씬을 나와도 텍스처가 남음 | 로드 소유자, 정적 참조, 에셋 핸들 |
| 메모리는 일정하지만 장시간 뒤 느려짐 | 발열·프레임 부하 등 다른 원인 |
같은 동작을 반복하고 스냅샷을 비교한다
예를 들어 상점을 열 때 문제가 난다고 합시다. 첫 진입 때 셰이더나 리소스를 준비할 수 있으므로 한 번 열고 닫아 초기화를 마칩니다. 안정된 시점에 A를 찍고, 상점을 열고 닫는 같은 동작을 여러 번 반복한 뒤 같은 시점에 B를 찍습니다.
Memory Profiler에서 늘어난 객체의 종류와 개수를 확인하고, 왜 아직 참조되는지 따라갑니다. 단순히 B의 전체 숫자가 크다는 것만으로 누수라고 결론내리지 않습니다. 풀을 최대 크기까지 채운 뒤에는 증가가 멈추는지, 또 한 차례 반복한 C에서도 계속 늘어나는지 보면 구분에 도움이 됩니다.
스냅샷 자체에도 시간과 메모리 비용이 있습니다. 연속으로 찍은 탓에 생긴 변화와 원래 게임의 변화를 혼동하지 않도록 측정 간격과 상태를 맞춥니다.
고칠 때는 자원을 소유한 곳을 찾는다
반복 호출되는 GetComponent는 참조를 캐시해 탐색을 줄일 수 있습니다. 다만 호출할 때마다 모든 환경에서 같은 GC 할당이 발생한다고 단정하면 안 됩니다. Editor와 Player, 성공·실패 경로에 따라 달라질 수 있으므로 실제 대상 빌드의 호출 스택을 봅니다.
이벤트 구독도 생명주기를 맞춥니다. 활성화 때 구독했다면 비활성화 때 해제할지, 객체 생성부터 파괴까지 유지할지 결정해야 합니다. 풀링 객체가 반복 활성화될 때 같은 이벤트를 중복 구독하지 않는지도 확인합니다.
Addressables를 쓴다면 로드한 핸들의 소유자가 언제 해제할지 정해야 합니다. GC가 C# 객체를 정리한다고 모든 Unity 네이티브 리소스와 에셋이 자동으로 원하는 시점에 내려가는 것은 아닙니다. 무조건 GC.Collect를 호출해 증상을 덮기보다는 참조와 해제 책임을 추적하는 편이 낫습니다.
할당을 줄여도 풀은 무한히 늘리지 않는다
오브젝트 풀은 생성·파괴 반복을 줄이는 대신 객체를 보관합니다. 너무 크게 잡으면 평소 메모리가 늘어납니다. 최대 동시 사용량과 비활성 객체 수를 기록하고, 씬을 떠날 때 풀을 유지할 이유가 있는지 살펴봅니다.
목표는 ’new를 한 줄도 안 쓰기’가 아닙니다. 로딩 중 감당할 수 있는 할당과 전투 중 반복되는 할당을 구분하고, 문제가 생기는 구간을 줄이는 것입니다. CPU 대기 마커 하나만 보고 GPU 병목이나 성능 여유를 확정하지 않는 것도 같은 이유입니다. VSync, 프레임 제한, 스레드 간 대기까지 전체 타임라인을 함께 봐야 합니다.