언리얼 엔진의 UObject 계열 객체는 일반 C++ 객체와 다르게 언리얼 GC (Garbage Collection)의 관리를 받는다.
| 객체 종류 | 생성 방식 |
|---|---|
UObject |
NewObject<T>() |
AActor |
GetWorld()->SpawnActor<T>() |
UActorComponent |
CreateDefaultSubobject<T>(), NewObject<T>() |
UUserWidget |
CreateWidget<T>() |
이 함수들은 객체를 생성하면서 언리얼 객체 시스템에 등록한다.
이후 GC는 등록된 객체들을 대상으로 생존 여부를 판단한다.
Strong Reference
UPROPERTY()가 붙은 UObject 참조는 GC가 추적할 수 있는 참조가 된다.
UPROPERTY()
TObjectPtr<UMyObject> MyObject;
MyObject가 어떤 객체를 가리키고 있고, 그 참조를 가진 객체가 살아 있다면 GC는 이 연결을 따라간다.
| 상태 | GC 판단 |
|---|---|
루트에서 객체까지 UPROPERTY 경로가 있음 |
살아있는 객체 |
| 루트에서 객체까지 도달할 수 없음 | 수거 대상 |
| 일반 raw pointer만 존재 | GC가 참조로 인식하지 못함 |
UPROPERTY는 GC가 따라갈 수 있는 참조 경로를 만든다.
UCLASS()
class UMyOwner : public UObject
{
GENERATED_BODY()
public:
UPROPERTY()
TObjectPtr<UMyObject> ReferencedObject;
};
위 구조에서 UMyOwner가 살아 있고, ReferencedObject가 UPROPERTY로 연결되어 있다면 ReferencedObject도 도달 가능한 객체로 판단된다.
자동 nullptr 처리
UPROPERTY로 등록된 UObject 포인터는 참조 대상 객체가 GC에 의해 제거될 때 자동으로 nullptr로 정리될 수 있다.
다만 Actor의 경우 Destroy() 호출 직후 바로 메모리에서 삭제되는 것이 아니라 Pending Kill / Destroyed 상태가 될 수 있다.
따라서 Actor나 UObject 유효성 확인에는 단순 if (Ptr)보다 IsValid(Ptr)이 더 안전하다.
if (IsValid(MyActorPtr))
{
MyActorPtr->SomeFunction();
}
| 검사 방식 | 의미 |
|---|---|
if (Ptr) |
nullptr 여부만 확인 |
IsValid(Ptr) |
nullptr + 삭제 대기 상태까지 확인 |
UHT와 UPROPERTY 추적 정보
UPROPERTY()가 GC와 연결될 수 있는 이유는 UHT (UnrealHeaderTool) 때문이다.
컴파일 과정에서 UHT는 UPROPERTY()가 붙은 멤버를 분석하고, 리플렉션 정보를 생성한다.
UCLASS()
class AMyPlayer : public APawn
{
GENERATED_BODY()
UPROPERTY()
TObjectPtr<AMyActor> MyActorPtr;
};
UHT는 대략 다음 정보를 생성한다.
| 정보 | 설명 |
|---|---|
| 클래스 이름 | AMyPlayer |
| 프로퍼티 이름 | MyActorPtr |
| 프로퍼티 타입 | AMyActor 참조 |
| 메모리 위치 | 객체 내부에서 해당 멤버가 있는 오프셋 |
| GC 추적 여부 | UObject 참조로 추적 가능 |
GC는 런타임에 이 정보를 이용해 객체 내부의 UPROPERTY 포인터를 찾는다.
UObject 생성과 GC 등록
GC가 관리하려면 객체가 언리얼 객체 시스템에 등록되어야 한다.
UMyObject* Obj = NewObject<UMyObject>();
AMyActor* Actor = GetWorld()->SpawnActor<AMyActor>();
| 생성 방식 | GC 관리 |
|---|---|
NewObject<T>() |
관리됨 |
SpawnActor<T>() |
관리됨 |
CreateWidget<T>() |
관리됨 |
일반 new |
UObject 대상으로 사용하면 안 됨 |
UObject 계열을 일반 new로 생성하면 언리얼 객체 시스템, 리플렉션, GC와 정상적으로 연결되지 않는다.
Root Set
GC는 절대 삭제되면 안 되는 객체인 Root Set을 출발점으로 삼는다.
| Root Set 예시 | 설명 |
|---|---|
UGameInstance |
게임 실행 동안 유지 |
UWorld |
현재 월드 |
| 엔진 핵심 객체 | 엔진이 직접 관리하는 객체 |
AddToRoot() 된 객체 |
개발자가 명시적으로 루트에 추가한 객체 |
Root Set에 있는 객체는 GC 탐색의 시작점이 된다.
MyObject->AddToRoot();
AddToRoot()를 사용하면 해당 객체가 Root Set 등록되어 엔진이 절대 삭제하지 않는다.RemoveFromRoot()로 직접 제거하지 않으면 계속 살아 있기 때문에 남용하면 메모리 누수가 발생한다.
Reachability
GC는 Root Set에서 시작해서 UPROPERTY 참조를 따라갔을 때 해당 객체에 도달할 수 있는지를 본다.
| 판단 기준 | 결과 |
|---|---|
| Root Set에서 도달 가능 | 살아있는 객체 |
| Root Set에서 도달 불가능 | Garbage |
| 일반 포인터로만 참조 | 도달 경로로 인정되지 않음 |
flowchart LR
A[Root Set] --> B[UWorld]
B --> C[Actor]
C --> D[D: UPROPERTY 참조 UObject]
E[E : 일반 포인터만 있는 UObject]
D는 Root Set에서 UPROPERTY를 통해 도달 가능하므로 살아 있다.E는 일반 포인터로만 참조되고 있다면 GC가 도달 가능한 객체로 보지 못할 수 있다.
Mark and Sweep
언리얼 GC는 기본적으로 Mark and Sweep 방식으로 동작한다.
Mark
Root Set에서 시작해 살아있는 객체를 표시한다.
flowchart TD
A[Root Set] --> B[GameInstance]
A --> C[World]
C --> D[Actor]
D --> E[UPROPERTY Object]
E --> F[UPROPERTY Object]
| 단계 | 설명 |
|---|---|
| 1 | Root Set 객체를 살아있음으로 표시 |
| 2 | Root 객체의 UPROPERTY 참조를 탐색 |
| 3 | 참조된 UObject를 살아있음으로 표시 |
| 4 | 이 과정을 재귀적으로 반복 |
Sweep
Mark 단계에서 살아있음 표시를 받지 못한 객체를 정리한다.
| 단계 | 설명 |
|---|---|
| 1 | 등록된 모든 UObject 목록을 순회 |
| 2 | Mark 되지 않은 객체를 Garbage로 판단 |
| 3 | 해당 객체를 참조하는 UPROPERTY 포인터 정리 |
| 4 | 객체 파괴 절차 진행 |
| 5 | 메모리 해제 |
객체 파괴 과정에서는 BeginDestroy(), FinishDestroy() 같은 단계가 관여한다.
virtual void BeginDestroy() override;
일반 게임플레이 로직에서 직접 호출하는 함수는 아니다.
객체 해제 과정에서 엔진이 호출한다.
TObjectPtr
UE5에서는 UObject 멤버 포인터에 TObjectPtr<T> 사용이 권장된다.
UPROPERTY()
TObjectPtr<UStaticMeshComponent> MeshComponent;
| 구분 | 설명 |
|---|---|
UObject* |
기존 raw UObject 포인터 |
TObjectPtr<T> |
UE5에서 권장되는 UObject 포인터 래퍼 |
UPROPERTY() TObjectPtr<T> |
GC 추적 가능한 안전한 멤버 참조 |
Actor Destroy와 GC
AActor::Destroy()는 Actor를 월드에서 제거 대상으로 표시하고, 엔진의 라이프사이클에 따라 정리되도록 만든다.
Destroy() 이후에는 포인터가 아직 nullptr이 아닐 수 있다.
하지만 객체는 더 이상 유효한 게임플레이 대상으로 보기 어렵다.
그렇기 때문에 IsValid()로 검사하는 습관이 안전하다.
| 상황 | if (Ptr) |
IsValid(Ptr) |
|---|---|---|
| 정상 객체 | true | true |
| nullptr | false | false |
| Destroy된 Actor | true일 수 있음 | false |
| GC 후 null 처리 | false | false |
UPROPERTY와 메모리 누수
UPROPERTY는 GC가 참조 관계를 정확히 파악하도록 해준다.
UPROPERTY()
TObjectPtr<UMyObject> MyObject;
이 참조가 남아 있는 동안 MyObject는 도달 가능한 객체가 된다.
반대로 더 이상 필요 없다면 참조를 끊어야 GC가 수거할 수 있다.
MyObject = nullptr;
| 상태 | 결과 |
|---|---|
UPROPERTY 참조 유지 |
객체가 살아남을 수 있음 |
참조를 nullptr로 변경 |
다음 GC에서 수거 가능 |
AddToRoot() 후 해제 안 함 |
계속 살아남음 |
| 순환 참조 | 구조에 따라 수거 지연 또는 유지 가능 |
GC가 있다고 해서 설계 없이 참조를 계속 붙잡아도 되는 것은 아니다.
불필요한 강한 참조는 객체를 계속 살려둘 수 있다.
흐름 요약
flowchart TD
A[NewObject / SpawnActor] --> B[UObject 시스템에 등록]
B --> C[UPROPERTY 참조 설정]
C --> D[GC 실행]
D --> E[Root Set에서 탐색 시작]
E --> F[UPROPERTY 참조를 따라 Mark]
F --> G{도달 가능한가?}
G -- Yes --> H[살아있는 객체로 유지]
G -- No --> I[Garbage로 판단]
I --> J[UPROPERTY 참조 null 처리]
J --> K[BeginDestroy / FinishDestroy]
K --> L[메모리 해제]
최종 요약
| 상황 | 권장 |
|---|---|
| UObject를 멤버로 보관 | UPROPERTY() TObjectPtr<T> 사용 |
| Actor 참조 사용 전 검사 | IsValid() 사용 |
| UObject를 직접 생성 | NewObject<T>() 사용 |
| Actor 생성 | SpawnActor<T>() 사용 |
| 컴포넌트 기본 생성 | CreateDefaultSubobject<T>() 사용 |
| 필요 없어진 강한 참조 | nullptr로 끊기 |
AddToRoot() 사용 |
반드시 RemoveFromRoot() 짝 맞추기 |
| 개념 | 요약 |
|---|---|
| GC | UObject의 생존 여부를 자동으로 판단하고 정리하는 시스템 |
| UPROPERTY | GC가 멤버 참조를 추적할 수 있게 만드는 리플렉션 정보 |
| Root Set | GC 탐색의 시작점이 되는 항상 살아있는 객체 목록 |
| Reachability | Root Set에서 UPROPERTY 경로로 도달 가능한지 여부 |
| Mark | 살아있는 객체를 표시하는 단계 |
| Sweep | 표시되지 않은 객체를 정리하는 단계 |
| TObjectPtr | UE5에서 권장되는 UObject 멤버 포인터 래퍼 |
| IsValid | nullptr와 삭제 대기 상태를 함께 확인하는 유효성 검사 |
'Unreal Engine' 카테고리의 다른 글
| AssetManager ⇄ Bundle System 구조 수정 (0) | 2026.07.13 |
|---|---|
| AssetManager ⇄ Bundle System(Data Asset) (0) | 2026.07.09 |
| Unreal Engine Singleton (0) | 2026.07.07 |
| Unreal Engine Tick 최적화 (0) | 2026.07.06 |
| UE 애니메이션 리타게팅 (0) | 2026.07.03 |