Unity 메모리 & GC 최적화
Unity 게임에서 GC 스파이크는 프레임 드롭의 주요 원인 중 하나입니다. Mono와 IL2CPP 모두 Boehm GC(비세대적, Stop-The-World)를 기반으로 하며, 힙 할당이 누적되면 GC가 실행되는 시점에 수십 밀리초의 멈춤이 발생합니다.
GC 동작 원리
Section titled “GC 동작 원리”Unity는 두 가지 스크립팅 백엔드를 지원합니다.
| 항목 | Mono | IL2CPP |
|---|---|---|
| 컴파일 방식 | JIT (런타임) | AOT (빌드 시 C++로 변환) |
| GC 방식 | Boehm GC | Boehm GC (동일) |
| 성능 | 개발 빌드에 적합 | 배포 빌드 권장 |
| 플랫폼 지원 | PC·에디터 | iOS·콘솔 필수 |
GC가 실행되면 모든 관리형 스레드가 일시 정지됩니다 (Stop-The-World). 힙 할당을 줄이는 것이 근본적인 해결책입니다.
GC Alloc 주요 원인
Section titled “GC Alloc 주요 원인”1. Boxing
Section titled “1. Boxing”// ❌ int → object boxing 발생object boxed = 42;Dictionary<string, object> data = new();data["hp"] = 100; // 매 프레임 boxing
// ✅ 제네릭 사용Dictionary<string, int> data = new();data["hp"] = 100;2. 문자열 연결
Section titled “2. 문자열 연결”// ❌ 매 프레임 새 string 생성void Update(){ debugText.text = "HP: " + currentHp; // 16~32B 할당}
// ✅ StringBuilder 또는 TryFormat 사용readonly System.Text.StringBuilder _sb = new();
void Update(){ _sb.Clear(); _sb.Append("HP: "); _sb.Append(currentHp); debugText.text = _sb.ToString();}3. LINQ
Section titled “3. LINQ”// ❌ 매 호출 시 IEnumerable 래퍼 할당var aliveEnemies = enemies.Where(e => e.IsAlive).ToList();
// ✅ for 루프로 대체var aliveEnemies = new List<Enemy>(enemies.Count);for (int i = 0; i < enemies.Count; i++) if (enemies[i].IsAlive) aliveEnemies.Add(enemies[i]);4. 클로저 (Lambda 캡처)
Section titled “4. 클로저 (Lambda 캡처)”// ❌ 클로저 캡처 시 힙에 클래스 인스턴스 생성float threshold = 10f;enemies.Sort((a, b) => a.Distance(threshold).CompareTo(b.Distance(threshold)));
// ✅ IComparer 구조체로 할당 제거struct DistanceComparer : IComparer<Enemy>{ public float threshold; public int Compare(Enemy a, Enemy b) => a.Distance(threshold).CompareTo(b.Distance(threshold));}enemies.Sort(new DistanceComparer { threshold = 10f });5. foreach on non-generic collections
Section titled “5. foreach on non-generic collections”// ❌ ArrayList foreach → IEnumerator 박싱ArrayList list = new ArrayList();foreach (object item in list) { }
// ✅ List<T> 사용List<Enemy> list = new List<Enemy>();foreach (Enemy e in list) { }Object Pooling
Section titled “Object Pooling”Unity 2021부터 UnityEngine.Pool 네임스페이스에 공식 풀 API가 포함됩니다.
using UnityEngine;using UnityEngine.Pool;
public class BulletPool : MonoBehaviour{ [SerializeField] GameObject bulletPrefab;
IObjectPool<GameObject> _pool;
void Awake() { _pool = new ObjectPool<GameObject>( createFunc: () => Instantiate(bulletPrefab), actionOnGet: obj => obj.SetActive(true), actionOnRelease: obj => obj.SetActive(false), actionOnDestroy: Destroy, collectionCheck: false, // 중복 반환 체크 (에디터 디버깅용) defaultCapacity: 20, maxSize: 100 ); }
public GameObject Get() => _pool.Get(); public void Release(GameObject obj) => _pool.Release(obj);}총알이 화면 밖으로 나가거나 충돌하면 Destroy 대신 pool.Release(obj)를 호출합니다.
Incremental GC
Section titled “Incremental GC”Unity 2019.1부터 Incremental GC를 지원합니다. Stop-The-World 대신 GC 작업을 여러 프레임에 분산합니다.
설정 방법:
Project Settings → Player → Configuration → Use Incremental GC 체크
// 스크립트에서 GC 시간 제한 설정 (ms)UnityEngine.Scripting.GarbageCollector.incrementalTimeSliceNanoseconds = 3_000_000; // 3msIncremental GC는 스파이크를 완전히 없애지 않습니다. 근본 해결은 할당 자체를 줄이는 것입니다.
Memory Profiler 활용
Section titled “Memory Profiler 활용”Unity Memory Profiler 패키지를 설치하면 힙 스냅샷을 찍어 분석할 수 있습니다.
Window → Memory Profiler → Capture New Snapshot| 분석 항목 | 확인 내용 |
|---|---|
| Managed Heap | 전체 힙 크기 및 단편화 |
| Top Objects by Size | 메모리를 가장 많이 사용하는 객체 |
| Duplicated Assets | 중복 로드된 텍스처·메시 |
| Native Objects | RenderTexture·AudioClip 등 |
실무 GC 스파이크 원인과 해결
Section titled “실무 GC 스파이크 원인과 해결”| 증상 | 원인 | 해결 |
|---|---|---|
| Update마다 0.1~0.5KB 할당 | string + 또는 new List | StringBuilder / 풀 사용 |
| 씬 전환 시 수백 MB 스파이크 | 이전 씬 오브젝트 미해제 | Resources.UnloadUnusedAssets() |
| GetComponent 매 프레임 호출 | 캐싱 누락 | Awake에서 캐싱 |
Coroutine yield return new WaitForSeconds | 매 실행마다 WaitForSeconds 인스턴스 생성 | 캐싱 static readonly WaitForSeconds |
// ❌ 매 코루틴 호출마다 할당yield return new WaitForSeconds(1f);
// ✅ 재사용static readonly WaitForSeconds Wait1Sec = new WaitForSeconds(1f);yield return Wait1Sec;