1. 언리얼 엔진 가비지 컬렉션(Unreal Engine Garbage Collection)
언리얼 엔진의 가비지 컬렉션(GC)은 엔진의 핵심적인 메모리 관리 시스템입니다. C++는 기본적으로 자동 메모리 관리를 지원하지 않지만, 언리얼 엔진은 UObject라는 자체적인 객체 시스템 위에서 동작하며, 이 UObject 인스턴스들의 생명 주기를 자동으로 관리하기 위해 가비지 컬렉터를 도입했습니다.
게임과 같이 수많은 객체가 동적으로 생성되고 소멸되는 환경에서는 메모리 누수(Memory Leak)나 유효하지 않은 포인터에 접근하는 문제(Dangling Pointer)가 발생하기 쉽습니다. 언리얼 GC는 이러한 문제들을 개발자가 일일이 신경 쓰지 않도록 자동화하여 안정성과 개발 편의성을 크게 향상시킵니다.
GC의 핵심 원리는 "도달 가능성(Reachability)"입니다. 특정 객체가 '루트(Root)'로부터 시작되는 참조 체인을 통해 도달 가능한 상태라면 '살아있는(Live)' 객체로 판단하고, 그렇지 않다면 '쓰레기(Garbage)'로 간주하여 메모리에서 해제합니다.
2. 핵심 구성 요소 (Core Components)
언리얼 GC를 이해하기 위해서는 다음의 핵심 요소들을 알아야 합니다.
2-1. UObject
UObject는 언리얼 엔진 객체 시스템의 최상위 기본 클래스입니다. 액터(AActor), 컴포넌트(UActorComponent) 등 엔진의 주요 객체들은 모두 UObject를 상속받습니다. GC는 오직 UObject를 상속받은 객체들만을 대상으로 동작합니다. UObject는 내부에 자신의 클래스 정보, 외부 참조 카운트 등 GC에 필요한 다양한 메타데이터를 포함하고 있습니다.
2-2. UPROPERTY()
UPROPERTY() 매크로는 UObject 멤버 변수를 언리얼 엔진의 리플렉션 시스템에 등록하는 역할을 합니다. GC는 이 UPROPERTY()로 선언된 UObject 포인터 변수만을 참조 관계로 인식합니다.
// MyActor.h
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyActor.generated.h"
class UMyObject;
UCLASS()
class AMyActor : public AActor
{
GENERATED_BODY()
public:
// GC가 이 참조를 따라가서 MyObjectInstance가 살아있다고 판단함
UPROPERTY()
UMyObject* MyObjectInstance;
// UPROPERTY가 없으므로 GC는 이 참조를 인지하지 못함
// 만약 다른 곳에서 이 객체를 참조하지 않는다면, GC에 의해 소멸될 수 있음
UMyObject* UnmanagedObjectInstance;
};
만약 UObject를 가리키는 포인터를 UPROPERTY() 없이 일반 C++ 포인터로 선언하면, GC는 해당 참조를 추적할 수 없습니다. 그 결과, 다른 곳에서 강한 참조(Strong Reference)를 하고 있지 않다면 해당 객체는 쓰레기로 오인되어 메모리에서 해제될 수 있습니다.
2-3. 루트 셋 (Root Set)
루트 셋은 GC가 도달 가능성 분석을 시작하는 '출발점'이 되는 객체들의 집합입니다. 어떤 객체도 이 루트 셋을 참조하지 않는다면, 그 객체는 절대 도달할 수 없습니다. 대표적인 루트 셋 객체는 다음과 같습니다.
- 게임 엔진 자체 (
GEngine) - 게임 인스턴스 (
UGameInstance) - 월드 (
UWorld)와 그 월드에 속한 액터 목록 AddToRoot()함수를 통해 명시적으로 추가된 객체들
객체를 AddToRoot()에 추가하면, 다른 어떤 객체도 참조하지 않더라도 GC의 대상이 되지 않습니다. 하지만 이는 메모리 누수의 원인이 될 수 있으므로, 반드시 필요할 때만 사용하고 RemoveFromRoot()로 해제해야 합니다.
3. GC 동작 원리: Mark-and-Sweep
언리얼 GC는 대표적인 Mark-and-Sweep 알고리즘을 사용합니다. 이 과정은 크게 '마크(Mark)' 단계와 '스위프(Sweep)' 단계로 나뉩니다.
3-1. 마크 (Mark) 단계: 도달 가능한 객체 표시
- 루트 셋에서 시작: GC는 먼저 루트 셋에 포함된 모든 객체를 '도달 가능' 상태로 표시(Mark)합니다.
- 재귀적 탐색: 표시된 객체가
UPROPERTY()를 통해 참조하는 다른UObject들을 따라가며, 그 객체들 또한 '도달 가능' 상태로 표시합니다. - 탐색 완료: 이 과정을 모든 도달 가능한 객체를 방문할 때까지 재귀적으로 반복합니다. 이 탐색은
UObject의AddReferencedObjects함수를 통해 이루어지며, 리플렉션 시스템이 자동으로 생성한 코드가UPROPERTY멤버들을 처리합니다.
3-2. 스위프 (Sweep) 단계: 쓰레기 수거
- 전체 객체 순회: 마크 단계가 끝나면, GC는 엔진에 존재하는 모든
UObject의 목록(GUObjectArray)을 순회합니다. - 쓰레기 식별: 순회하면서 '도달 가능'으로 표시되지 않은 객체를 '쓰레기'로 식별합니다.
- 소멸 처리:
- 쓰레기로 식별된 객체들에 대해
ConditionalBeginDestroy()함수를 호출합니다. 이 함수는 객체가 곧 소멸될 것임을 알리고, 하위 객체 참조를 해제하거나 네이티브 리소스를 정리할 기회를 줍니다. - 이후 해당 객체를 참조하던 모든
UObject포인터들을nullptr로 만듭니다. - 마지막으로
FinishDestroy()를 호출하고 객체가 사용하던 메모리를 해제합니다.
- 쓰레기로 식별된 객체들에 대해
이 과정은 UWorld::Tick 내부에서 주기적으로 CollectGarbage() 함수를 호출함으로써 실행됩니다.
4. GC 동작 계층 구조
4-1. 사용자/게임플레이 레벨
- 객체 생성:
NewObject<UClassName>()이나GetWorld()->SpawnActor<AMyActor>()등을 통해UObject를 생성합니다. 이 객체들은 생성 시GUObjectArray에 등록됩니다. - 참조 생성:
UPROPERTY()가 붙은 포인터에 생성된 객체를 할당하여 참조 관계를 형성합니다. - 객체 소멸 요청:
AActor::Destroy()나UObject::MarkPendingKill()을 호출하여 객체를 명시적으로 파괴하도록 요청할 수 있습니다. 이는 객체의 참조를 즉시 끊는 것이 아니라, 다음 GC 사이클에서 수거되도록 'Pending Kill' 상태로 만드는 것입니다.
4-2. 엔진 레벨 (UWorld::Tick)
- 게임 루프의 일부인
UWorld::Tick에서 GC 실행 여부를 결정합니다. - 일정 시간이 지나면 (
gc.TimeBetweenPurgingPendingKillObjects콘솔 변수로 제어)CollectGarbage()함수를 호출하여 전체 GC 프로세스를 트리거합니다. CollectGarbage()는 내부적으로FRealtimeGC::PerformMainThreadGarbageCollection()을 호출하여 실제 GC 로직을 실행합니다.
4-3. 로우 레벨 (FRealtimeGC, GUObjectArray)
GUObjectArray: 엔진 내의 모든UObject인스턴스에 대한 정보를 담고 있는 전역 배열입니다. GC는 이 배열을 순회하며 객체들의 상태를 관리합니다.FRealtimeGC: 실제 Mark-and-Sweep 로직을 구현하는 클래스입니다.MarkObjectsAsReachable(): 마크 단계를 수행합니다.SweepObjects(): 스위프 단계를 수행합니다.
- 엔진 코드:
GarbageCollection.cpp파일에 GC의 핵심 구현이 포함되어 있습니다.FRealtimeGC클래스와 관련 함수들을 분석하면 GC의 상세한 동작을 파악할 수 있습니다.
5. 심화 주제 및 최적화
5-1. 클러스터링 (Clustering)
- 정의: 연관된
UObject들을 메모리 상에 함께 배치하는 최적화 기법입니다. 예를 들어, 하나의 액터와 그 액터가 소유한 여러 컴포넌트들은 하나의 클러스터로 묶일 수 있습니다. - 장점:
- 캐시 효율성: 관련된 데이터가 메모리에 인접해 있으므로 CPU 캐시 히트율이 높아져 접근 속도가 향상됩니다.
- GC 성능 향상: GC가 객체 참조를 따라갈 때 메모리의 여러 곳을 점프할 필요 없이 순차적으로 스캔할 수 있어 마크 단계의 속도가 빨라집니다.
- 동작: 액터가 생성될 때, 해당 액터와 그 서브오브젝트(컴포넌트 등)들은 가능하면 하나의 연속된 메모리 블록에 할당됩니다.
5-2. 점진적 GC (Incremental GC)
- 문제점: 풀 GC(Full GC)는 수십만 개의 객체를 한 번에 처리해야 하므로, 순간적으로 긴 시간(수십~수백 ms)이 소요되어 프레임 드랍(Hitch)을 유발할 수 있습니다.
- 해결책: Mark-and-Sweep의 '마크' 단계를 여러 프레임에 걸쳐 나누어 수행합니다.
gc.IncrementalReachabilityAnalysis콘솔 변수를true로 설정하여 활성화할 수 있습니다.- 매 프레임마다 정해진 시간만큼만 도달 가능성 분석을 수행하고, 전체 분석이 끝나면 한 번에 스위프 단계를 실행합니다.
- 이를 통해 GC로 인한 프레임 드랍을 크게 완화할 수 있습니다.
5-3. FGCObject
UObject가 아닌 일반 C++ 클래스에서UObject에 대한 강한 참조를 유지해야 할 때 사용하는 인터페이스입니다.FGCObject를 상속받고AddReferencedObjects가상 함수를 구현하면, GC가 마크 단계에서 이 함수를 호출하여 해당 클래스가 참조하는UObject들을 도달 가능하다고 표시합니다.- 주로 매니저 클래스나 서브시스템 등,
UObject시스템 외부에 존재하면서UObject의 생명 주기에 관여해야 할 때 유용합니다.
// MyManager.h
#include "UObject/GCObject.h"
class UMyObject;
class FMyManager : public FGCObject
{
public:
// FGCObject 인터페이스 구현
virtual void AddReferencedObjects(FReferenceCollector& Collector) override;
// UObject에 대한 강한 참조
UMyObject* StrongRefObject;
};
// MyManager.cpp
void FMyManager::AddReferencedObjects(FReferenceCollector& Collector)
{
// Collector에 참조를 추가하여 GC가 이 객체를 살아있는 것으로 판단하게 함
Collector.AddReferencedObject(StrongRefObject);
}
5-4. 약한 참조 (FWeakObjectPtr, TWeakObjectPtr)
FWeakObjectPtr는UObject를 참조하지만, 그 참조가 객체의 생명 주기에 영향을 주지 않는(GC를 막지 않는) 스마트 포인터입니다.- 만약 참조하던 객체가 GC에 의해 파괴되면,
FWeakObjectPtr는 자동으로nullptr가 되어 Dangling Pointer 문제를 방지합니다. - 사용 사례: 필수가 아닌 선택적 참조나, 객체에 대한 캐싱(Caching) 구현 시 순환 참조를 방지하고자 할 때 유용합니다.
6. 요약
언리얼 엔진의 가비지 컬렉션은 UObject 기반 객체들의 메모리를 자동으로 관리하는 시스템입니다. '루트 셋'이라는 최상위 객체들로부터 시작하여, UPROPERTY 매크로로 표시된 참조들을 따라가며 도달 가능한 모든 객체를 '살아있음'으로 표시합니다(마크 단계). 이 과정이 끝나면, 표시되지 않은 모든 객체는 '쓰레기'로 간주되어 메모리에서 해제됩니다(스위프 단계). 이 'Mark-and-Sweep' 방식은 개발자가 메모리 누수나 Dangling Pointer 걱정 없이 로직 개발에 집중할 수 있게 해줍니다. 성능 저하를 막기 위해, 마크 단계를 여러 프레임에 나누어 처리하는 '점진적 GC'와 연관 객체를 메모리에 모아두는 '클러스터링' 같은 최적화 기법도 사용됩니다.
언리얼 GC는 어떻게 동작하나요?
언리얼 GC는 'Mark-and-Sweep' 알고리즘을 사용합니다. 루트 셋(Root Set)에서 시작해 UPROPERTY로 연결된 모든 UObject 참조를 탐색하며 도달 가능한 객체들을 마킹합니다. 탐색이 끝난 후, 마킹되지 않은 객체들을 쓰레기로 간주하고 메모리에서 제거합니다.
UObject 포인터 멤버에 UPROPERTY()를 붙이지 않으면 어떻게 되나요?
GC가 해당 참조를 인지하지 못합니다. 따라서 다른 곳에서 그 객체를 강하게 참조하고 있지 않다면, GC는 그 객체가 더 이상 필요 없다고 판단하여 메모리에서 해제할 수 있습니다. 이는 예기치 않은 크래시로 이어지는 Dangling Pointer 문제의 주된 원인이 됩니다.
GC로 인한 프레임 드랍(Hitch)을 어떻게 완화할 수 있나요?
첫째, '점진적 GC(Incremental GC)'를 활성화하여 마크 단계를 여러 프레임에 분산시킬 수 있습니다.
둘째, UObject를 불필요하게 많이 생성하지 않고, 가능하다면 일반 C++ 객체나 구조체(USTRUCT)를 사용하는 것이 좋습니다.
셋째, 액터나 객체를 자주 생성하고 파괴하는 대신 '오브젝트 풀링(Object Pooling)' 기법을 사용하여 재활용하는 방법도 효과적입니다.
마지막으로, 언리얼 인사이트(Unreal Insights)와 같은 프로파일링 툴로 GC 부담이 큰 부분을 찾아 최적화해야 합니다.
FGCObject는 언제 사용하나요?
UObject가 아닌 일반 C++ 클래스가 UObject에 대한 강한 참조를 안전하게 유지해야 할 때 사용합니다. FGCObject 인터페이스를 구현하고 AddReferencedObjects 함수 안에 참조하는 UObject를 등록하면, GC가 해당 UObject를 쓰레기로 수집하지 않습니다. 주로 전역 매니저나 특정 시스템에서 UObject의 생명 주기를 보장해야 할 때 유용합니다.
'Unreal' 카테고리의 다른 글
| 캐릭터의 상대 위치 판단 방법 (0) | 2025.11.28 |
|---|---|
| bUseControllerRotation과 bUseControllerDesiredRotation의 차이 (0) | 2025.11.26 |
| NetMode, NetConnection, NetDriver, NetRole (0) | 2025.11.14 |
| 콜리전 필터링 Collision Filtering (0) | 2025.11.03 |
| UE5 Delegate와 Event의 차이점 (0) | 2025.10.31 |
