게임 개발/유니티 엔진

유니티와 C#에서의 GC (Garbage Collection)

술단고 2025. 3. 24. 00:54

Photo By Gary Chan

 

가비지 컬렉션(Garbage Collection)이란?

게임을 개발하다보면 당연히 무언가를 만들게 된다.

그것이 플레이어든, 몬스터든, 아무튼 오브젝트를 만들어 게임 세상에 두게 된다.

객체지향 프로그래밍(OOP)에서, 그런 사물 하나하나들을 객체라고 부른다.

 

'new' 키워드를 써서 객체를 메모리 어딘가에 만드는 것 까진 좋은데, 게임에서 그 객체가 역할을 다 하거나, 게임이 종료되었다면 더 이상 그 객체가 메모리에 있어선 안된다.

메모리에 계속 남아있다면, 우리의 램은 32GB정도로는 어지간한 프로그램은 조금 돌리다 터져버릴 것이다.

 

즉, 더 이상 사용하지 않게 된 데이터는 메모리만 차지하는 가비지(Garbage)가 된다.

그렇다면, 이 가비지를 알아서 찾아서 정리해주는 자동 청소부가 가비지 컬렉션(GC)이 되겠다.

옛날엔 동적으로 할당해준 모든 객체들을 직접 메모리에서 프로그래머가 일일이 해제해줬지만, 이젠 이런것도 프로그래밍 언어가 알아서 해주는 좋은 시대가 온 것이다.

 

힙(Heap)과 스택(Stack) 메모리

스택은 우리가 알고있는 그 FILO의 자료구조와 이름이 같다. 실제로 그 스택처럼 동작하기도 한다.

조금 쉽게 풀어쓰자면, 힙 영역은 new로 만든 객체가 저장되며 GC가 관리하는 영역이고, 스택 영역은 지역 변수들이 모이는 영역이며 함수가 return되면 알아서 변수가 반환되는 영역이다.

 

구분 힙(Heap) 스택(Stack)
저장되는 것 new로 만든 객체 등 지역 변수, 매개변수 등
메모리 관리 GC가 관리 함수 호출/종료 시 자동 관리
속도 느림 (GC 필요) 빠름 (자동 반환)
GC 대상 O X

 

즉, 스택 영역을 다룰 땐 비교적 자유롭게 마구 사용해도 되지만,

힙 영역을 다룰 땐 비교적 신중하게 메모리를 사용하는 것이 중요하다.

 

C#과 GC

C# 프로그래밍 언어에서의 GC는 .NET 런타임이 관리한다.

닷넷이라는 단어가 생소할 수 있으니 간단하게 말하자면, 마이크로소프트에서 개발한 프로그램 개발 및 실행 환경이다.

개발자라면 한 번 쯤 들어봤겠지만, C#은 마이크로소프트가 개발한 것이다!

 

아무튼 .NET 런타임은 객체를 단순히 추적하지 않고, 오래 살아남은 객체들은 비교적 덜 검사하고, 새로 생긴 객체를 위주로 검사하며 가비지 대상인지 확인한다.

이렇게 장수한 객체, 새로 태어난 객체를 지칭하는 개념을 세대(Generation)라고 한다.

 

여기에 GC 알고리즘으로는 Mark-Sweep-Compact 알고리즘을 사용하는데, 이는 최상위 루트 객체에서 출발하여, 루트 객체를 참조하는 모든 객체를 재귀적으로 순회하며, 루트 참조가 있는 경우 식별(Mark)하고 다음 객체로 넘어간 후, 힙 전체를 스캔한 뒤 식별되지 않는 객체를 제거(Sweep) 하는 알고리즘이다. 즉 최상위 루트 객체와 어떻게든 건너건너 참조되어있는 객체라면 Mark되고, Mark되지 않은 객체는 Sweep한다.

 

또 GC가 돌며 가비지를 회수할 땐, 잠시 프로그램이 멈춰서 메모리를 정리하는데, 이 잠깐 멈추는 시간대를 'Stop the World'라고 표현한다. 더불어 이런 GC가 한 번씩 발생하는 과정에서 성능이 급격하게 저하되는 현상을 GC 스파이크(GC Spike)라고도 부른다.

 

GC를 얘기하면 또 메모리 단편화 얘기를 안 할 수가 없는데, 이렇게되면 너무 깊고 복잡해지니 단편화에 대한 내용은 추후에 따로 포스팅하겠다. 컴퓨터구조와 운영체제, 자료구조와 알고리즘이 이래서 어렵지만 재밌는 것이다.

 

Unity에서의 GC

당연히 Unity도 C# 기반이기 때문에 .NET의 GC 시스템을 그대로 쓴다.

Unity에선 Managed Heap이라는 영역만 GC가 관리하며, Texture, Mesh, AudioClip같은 Native 리소스는 GC 관리 대상이 아니다.

 

아래의 상황에서 GC가 자주 발생하는 편이다.

 

  • string을 계속 새로 만든다거나 (string.Concat, + 연산)
  • Instantiate, Destroy를 반복
  • foreach로 순회하면서 Boxing이 발생하는 경우

 

GC 관리를 위해선 위 사항들을 잘 케어하면 좋은데, 문자열을 합칠 땐 StringBuilder를 사용하고, 프리팹을 마구 생성하고 Destroy하기보단 오브젝트 풀링을 사용하며, foreach 등으로 박싱(Boxing)을 유도하기보단 for문을 사용하는 것이 좋다.

 

현대에 와서는 아무래도 하드웨어의 사양과 제원이 좋아지면서 작은 캐주얼 게임이라면 굳이 GC를 엄격하게 관리할 필요는 없다는 것이 몇몇 개발자들의 의견이지만, 반대로 이제는 1초에 60프레임을 넘어 1초에 120프레임은 기본으로 뽑아내는 시대이기 때문에, 모바일 등의 환경에서 GC 스파이크가 체감되지 않도록 최적화에 신경을 쓰는 것은 무척이나 중요하다고 생각한다.

 

GC를 잘 이해하고 좋은 자료구조와 알고리즘을 선택해서, 메모리 누수도, GC 스파이크 등의 렉도 없는 부드럽고 끊김 없는 게임을 만드는 개발자가 되어보자.

 

 

 

 

가비지 컬렉터 개요 - Unity 매뉴얼

Unity에서는 가비지 컬렉터를 사용하여 애플리케이션과 Unity에서 더 이상 사용하지 않는 오브젝트로부터 메모리를 회수합니다. 스크립트가 관리되는 힙에 할당하려고 하지만 할당을 수용할 수

docs.unity3d.com