이번에 다룰 내용은 지난번에 다룬 가비지 컬렉션이 실제 엔진에서 어떻게 사용되는지 조금 더 자세하게 알아보겠습니다.
언리얼은 C++을 사용하는 엔진으로 언어 자체적으로 가비지 컬렉션이 구현되어 있지 않습니다.
이에 에픽 게임즈에서는 자체적으로 가비지 컬렉션을 구현해 두었습니다.
C++의 기본적인 메모리 관리 방법만으로 관리하기엔 무수히 많은 객체들이 생성되고 삭제되는 게임 환경에서 실수의 여지가 많은 만큼 단순히 기본 할당 해제만을 사용하는것은 위험의 소지가 다분하고 에디터 또는 블루프린트와도 코드가 연계 되어야 하기 때문에 일관적인 메모리 관리가 필요하기 때문입니다.
이러한 문제들을 해결하기 위해 언리얼에서는 프로퍼티 시스템이라 불리는 리플렉션을 사용하는데
이 내용에 대해서는 추후 다루도록 하겠습니다.
대략적으로 런타임에 프로그램이 자기 자신의 구조를 탐색, 수정할 수 있는 기능으로 C++의 RTTI 와 유사한 역할을 합니다.
다만 둘의 목적성이나 실제 활용의 정도 차이가 매우 크기 때문에 런타임에서 객체의 정보를 가진다 라는 것을 핵심으로 이해하면 되겠습니다.
https://www.unrealengine.com/ko/blog/unreal-property-system-reflection
해당 링크를 통해 간략하게 파악 할 수 있습니다.
우선 언리얼의 GC를 사용하기 위해선 UPROPERTY가 필요합니다.
GC의 대상은 UOBJECT 이며 UPROPERTY가 아닌 UOBJECT는 GC가 참조를 인식하지 못해,
여전히 사용 중인 객체도 해제될 수 있습니다.
C++은 포인터가 어떤 객체가 어떤 것을 참조하는지 또는 유효한지 등의 정보를 런타임 단계에서 추적할 수 없습니다.
그 정보를 알기 위해 언리얼에서는 UPROPERTY를 이용합니다.
UPROPERTY로 지정된 변수는 UHT(언리얼 헤더 툴)을 통해 FPROPERTY 데이터에 들어가게 됩니다.
이 데이터를 순회하면서 UOBJECT를 참조하는 값을 인지하여 마크 하는 방식을 사용합니다.
언리얼의 GC는 마크 앤 스윕 방식입니다.
우선 루트 셋을 설정합니다.
그 루트 셋을 바탕으로 UPROPERTY로 등록된 변수만을 탐색하면서 참조를 확인합니다.
탐색된 객체는 마크를 해둡니다.
마크 단계에서는 모든 UOBJECT를 탐색하는 것이 아니라 UPROPERTY만을 탐색하여 참조된 객체 만을 확인 합니다.
그 후 스윕 과정에서 GUObjectArray를 순회하면서 모든 마크 되지 않은 객체를 해제해줍니다.
마크 된 객체들은 마크를 초기화 해줍니다.
언리얼은 컴팩트 단계는 거치지 않는데 이는 컴팩트 과정에서 참조들을 모두 업데이트 해주어야 하는데
참조의 수가 무수히 많을 수 있는 게임의 환경 상 이를 수행하는 것이 어렵습니다.
또한 이 작업은 대규모 메모리 복사를 수행해야 하는데 성능적으로도 영향을 끼칠 수 있습니다.
이러한 이유로 언리얼 GC에서는 별도의 컴팩트 과정을 수행하지 않습니다.
대신 GUObjectArray의 칸을 비우고 재사용하는 방식을 사용합니다.

이해를 돕기 위한 이미지입니다.
이러한 과정을 통해 마크 앤 스윕을 거쳐 GC가 작동하게 됩니다,

해당 이미지를 통해서 언리얼에서 액터들이 어떤 수명 주기를 가지는지,
그 과정에서 GC가 어떤 타이밍에 작동하는지 알 수 있습니다.
GC의 잘못된 사용은 성능적 저하를 불러일으킵니다.
그러한 문제를 해결하기 위한 다양한 방법들이 있습니다.
언리얼의 GC 또한 유니티와 마찬가지로 Stop-The-World 현상이 GC를 실행할 때 일어나고
이를 위해 Incremental GC를 사용합니다.
해당 방식은 한 프레임에 GC를 실행하는 것이 아니라 GC의 과정을 분할하여
프레임 드랍을 완화하고 성능적으로 더 나은 퍼포먼스를 보일 수 있도록 하는 효과가 있습니다.
오브젝트 풀링 기법을 이용하여 할당, 해제를 해야하는 오브젝트를 생성하는 대신에 재사용하는 방법도 잘 알려진 방법입니다.
Non-UObject를 사용하거나 필요하지 않다면 UPROPERTY로 지정하지 않는 것도 중요한 방법입니다.
호출 시점을 지정하여 로딩이나 프레임 드랍이 발생하지 않을만한 시점에서 미리 실행시켜주는 방법도 존재합니다.
GC는 강력한 메모리 관리 도구이며 개발자의 실수를 보완해 줍니다.
그와 동시에 성능적 이슈의 가능성 또한 발생합니다.
강력한 도구를 잘 사용하기 위해선 작동 원리와 문제점을 명확히 인식하고 그에 맞는 개발을 수행하는 것이
실력있는 개발자에게 필수적으로 요구되는 덕목이라 생각합니다.