가비지 컬렉터(Garbage Collector, GC)는 더 이상 사용되지 않는 메모리를 자동으로 회수하여 프로그램의 메모리 누수를 방지하는 기능입니다. C#과 유니티(Unity)에서는 .NET의 GC 시스템을 활용하여 자동으로 메모리를 관리합니다.
하지만, GC의 동작 방식과 최적화 방법을 이해하지 못하면 **프레임 드롭(Frame Drop)**이나 불필요한 메모리 사용 증가와 같은 성능 저하 문제가 발생할 수 있습니다.
C#에서의 가비지 컬렉터 동작 원리
C#의 GC는 세대별(Generational) 관리 방식을 사용하여 메모리를 최적화합니다.
- Gen 0 (1세대): 새로 생성된 객체 (GC가 자주 실행됨)
- Gen 1 (2세대): 한 번 GC를 통과한 객체
- Gen 2 (3세대): 장기간 유지되는 객체 (GC 실행 빈도가 낮음)
GC는 특정 시점에서 불필요한 객체를 제거하여 메모리를 확보하며, Mark and Sweep과 Compact 방식으로 작동합니다.
GC의 동작 원리: Mark and Sweep, Compact
1. Mark and Sweep (마크 앤 스윕) 방식
Mark and Sweep은 GC의 핵심 동작 방식으로, 불필요한 객체를 찾아 제거하는 과정입니다.
Mark 단계
- 루트(Root) 객체(스택, 정적 변수, 전역 객체 등)에서 접근 가능한 모든 객체를 탐색합니다.
- 접근할 수 있는 객체(사용 중인 객체)에 "마크(Mark)" 표시를 합니다.
- 접근할 수 없는 객체(사용되지 않는 객체)는 마크되지 않습니다.
Sweep 단계
- 마크되지 않은 객체(사용되지 않는 객체)를 메모리에서 제거(Free)합니다.
- 마크된 객체는 그대로 유지됩니다.
장점: 단순한 알고리즘으로 빠르게 실행됨
단점: 객체 삭제 후 메모리가 단편화(Fragmentation)될 수 있음
2. Compact (압축) 방식
Mark and Sweep 방식의 문제점 중 하나는 메모리 단편화(Fragmentation) 입니다.
Compact 단계
- Mark and Sweep 이후, 남아 있는 객체들을 한쪽으로 이동시켜 연속된 메모리 공간을 확보합니다.
- 메모리의 빈 공간을 정리하고 압축하여 단편화를 해결합니다.
- 객체의 참조 주소가 변경될 수 있으므로, 모든 참조를 업데이트합니다.
장점: 메모리 단편화 문제를 해결하여 성능 최적화 가능
단점: 객체를 이동하는 과정에서 추가적인 연산 비용이 발생
3. 유니티에서 사용하는 GC 알고리즘
유니티는 기본적으로 .NET의 가비지 컬렉터(GC) 를 사용하며, 다양한 GC 알고리즘을 채택하여 최적화를 수행합니다.
1. Boehm GC
- 유니티 2018 이전 버전에서 사용된 보수적(Conservative) GC 방식
- Stop-the-world 방식으로 실행되어 게임이 순간적으로 멈출 수 있음
- Mark and Sweep 기반으로 동작하며, 정확한 객체 추적이 어려움
2. Incremental GC (증분 GC)
- 유니티 2019 이후부터 도입된 GC 방식
- GC 작업을 여러 프레임에 걸쳐 나누어 수행하여 프레임 드롭을 줄임
- 실시간 게임에서도 부드러운 프레임 유지 가능
3. Unity's Managed GC
- 최신 유니티 버전에서는 더 효율적인 메모리 관리와 GC 수행이 가능하도록 최적화됨
- .NET Core 및 .NET 5 이상의 기술을 점진적으로 통합하며 GC 성능 개선
유니티에서 GC 최적화 방법
1. 불필요한 객체 생성 방지
- Update() 안에서 new 키워드 사용을 최소화합니다.
- 불필요한 string 연산을 피하고 StringBuilder를 활용합니다.
2. 객체 풀링(Object Pooling) 활용
public class ObjectPool
{
private Queue<GameObject> pool = new Queue<GameObject>();
private GameObject prefab;
public ObjectPool(GameObject prefab, int initialSize)
{
this.prefab = prefab;
for (int i = 0; i < initialSize; i++)
{
var obj = GameObject.Instantiate(prefab);
obj.SetActive(false);
pool.Enqueue(obj);
}
}
public GameObject GetObject()
{
if (pool.Count > 0)
{
var obj = pool.Dequeue();
obj.SetActive(true);
return obj;
}
return GameObject.Instantiate(prefab);
}
public void ReturnObject(GameObject obj)
{
obj.SetActive(false);
pool.Enqueue(obj);
}
}
3. Incremental GC 활성화
- Edit → Project Settings → Player → Other Settings → Use Incremental GC 활성화
- C# 코드에서 직접 설정 가능
using UnityEngine;
public class GCSettingsExample : MonoBehaviour
{
void Start()
{
System.GCSettings.LatencyMode = System.Runtime.GCLatencyMode.Interactive;
}
}
Q&A: 자주 묻는 질문
Q1: 유니티에서 GC가 실행될 때 프레임 드롭이 발생하는 이유는?
A: 기본적으로 GC는 Stop-the-world 방식으로 동작하여 실행 중인 게임 로직이 멈출 수 있습니다. Incremental GC를 활성화하면 프레임 드롭을 최소화할 수 있습니다.
Q2: Boehm GC란 무엇인가요?
A: 유니티 2018 이전에 사용된 GC로, Conservative GC 방식을 사용하여 포인터를 추적해 객체를 정리합니다. Stop-the-world 방식이기 때문에 성능 문제가 발생할 수 있습니다.
Q3: 유니티에서 GC를 강제로 실행하면 안 되는 이유는?
A: GC.Collect()는 강제적으로 메모리를 회수하지만, 실행 시 프레임 드롭이 발생할 수 있으므로 신중하게 사용해야 합니다.
결론
Boehm GC는 Stop-the-world 방식을 사용하여 유니티에서 성능 저하를 유발할 수 있습니다.
유니티 2019 이후부터는 Incremental GC(증분 GC) 를 도입하여 프레임 드롭을 줄였습니다.
최적화 방법 요약
- 객체 풀링(Object Pooling) 사용
- 불필요한 객체 생성 최소화
- Incremental GC 활성화
- 메모리 단편화 방지 (Compact 방식 활용)
적절한 메모리 관리 기법을 적용하면 부드러운 게임 플레이와 성능 향상을 기대할 수 있습니다!