Unreal Engine GC는 어떻게 UObject를 추적할까?

//
//

🚀 들어가며

이전 글에서는 Unreal Engine이 왜 C++ 기본 RTTI 대신 자체 Reflection 시스템을 사용하는지 살펴봤습니다.

언리얼은 단순 타입 확인을 넘어서

  • 에디터
  • 블루프린트
  • 직렬화
  • 네트워크 복제
  • 가비지 컬렉션(GC)

같은 엔진 전체 기능과 연결되기 때문에, 자체적인 메타데이터 시스템이 필요했습니다.

언리얼의 메타 시스템을 이해하는 과정에서 아래 질문을 해볼 수 있습니다.

❓ Unreal Engine의 GC는 어떻게 UObject를 추적할까?

언리얼 엔진을 공부하다 보면 메모리 관리 측면에서 다음의 말을 들어보셨을 겁니다.

  • UPROPERTY 안 붙이면 위험하다
  • GC가 추적하지 못한다
  • UObject가 갑자기 nullptr 된다

처음에는 다소 이상하게 느껴질 수 있습니다.

왜냐하면 C++에서는 보통

  • new
  • delete
  • std::shared_ptr

같은 방식으로 메모리를 관리하기 때문입니다.

그런데 언리얼 엔진은 조금 다릅니다.

언리얼의 UObject 시스템은 👉 Reflection 시스템과 연결된 자체 GC 구조로 동작합니다.

이번 글에서는

  • 언리얼 GC가 왜 필요한지
  • GC가 UObject를 어떻게 추적하는지
  • 왜 UPROPERTY가 중요한지
  • 왜 raw pointer만으로는 부족한지

정리해보겠습니다.


🧠 C++는 기본적으로 GC가 없습니다

일반적인 C++에서는 객체 생명주기를 개발자가 직접 관리합니다.

Player* player = new Player();

delete player;

또는 스마트 포인터를 사용합니다.

std::shared_ptr<Player> player = std::make_shared<Player>();

즉 기본적으로

  • 누가 객체를 소유하는가?
  • 언제 삭제할 것인가?

를 개발자가 직접 결정합니다.

하지만 게임 엔진에서는 객체 관계가 매우 복잡해집니다.

예를 들어

Actor
 ├── Component
 ├── Material
 ├── Animation
 ├── UI Widget
 └── 다른 UObject 참조

수많은 객체가 서로 연결됩니다.

그리고 게임 진행 및 게임 개발 과정에서

  • 레벨 전환
  • 블루프린트 생성
  • 에디터 수정
  • 런타임 Spawn

등이 계속 일어납니다.

이 상황에서 모든 객체의 생명주기를 수동 관리하는 것은 매우 어렵습니다.

그래서 Unreal Engine은 UObject 계열 객체에 대해 👉 자체 Garbage Collection 시스템을 사용합니다.


//

🎯 언리얼 GC의 핵심 아이디어

지금까지 설명한 Unreal Engine GC 구조를 그림으로 정리해보면 다음과 같습니다.

핵심은 언리얼 GC는 Reflection 시스템과 UPROPERTY 정보를 기반으로 도달 가능한 UObject를 추적한다는 점입니다.

Unreal Engine GC가 Reflection 시스템과 UPROPERTY를 이용해 UObject를 추적하는 과정을 설명하는 인포그래픽
Unreal Engine의 Garbage Collection은 Reflection 시스템과 UPROPERTY 정보를 기반으로 Reachable UObject를 추적하고 관리합니다.

위 구조처럼 Unreal Engine의 Garbage Collection은 단순 메모리 해제가 아니라, UObject 참조 그래프를 순회하는 방식으로 동작합니다.

특히

  • UPROPERTY 기반 참조 추적
  • Reachability 분석
  • Mark & Sweep 구조
  • TWeakObjectPtr 기반 안전한 약한 참조

구조가 서로 연결되면서 엔진 전체 객체 생명주기를 관리하게 됩니다.


🧩 Reachability(도달 가능성)

GC에서 가장 중요한 개념은 Reachability입니다.

예를 들어

GameInstance
 └── PlayerController
      └── Character
           └── Weapon

이렇게 연결되어 있다면

  • GameInstance가 살아있고
  • PlayerController를 참조하고 있으며
  • Character를 참조하고 있고
  • Weapon까지 연결되어 있다면

Weapon도 살아있는 객체로 판단됩니다.

루트 객체
↓
참조 순회
↓
도달 가능한 UObject 표시
↓
나머지 제거

흐름으로 동작합니다.


🎮 그런데 GC는 객체를 어떻게 찾을까?

여기서 가장 중요한 질문이 등장합니다.

❓ GC는 UObject 참조를 어떻게 알 수 있을까?

예를 들어

class AMyActor : public AActor
{
public:
    UObject* Target;
};

이 코드를 보면, GC 입장에서는 문제가 있습니다.

  • Target이 UObject인지
  • 단순 포인터인지
  • GC 추적 대상인지

알 수 없습니다.

왜냐하면 C++ 컴파일 이후에는 👉 일반 원시 포인터(raw pointer)는 단순 주소 값일 뿐이기 때문입니다.

0x12345678

같은 주소만 존재합니다.

GC는

  • 이 포인터가 UObject인지
  • 참조 관계인지
  • 추적해야 하는지

자동으로 알 수 없습니다.

그래서 등장하는 것이 바로 👉 UPROPERTY() 매크로입니다.


🔥 UPROPERTY()가 중요한 이유

언리얼에서는 UObject 참조 변수를 보통 이렇게 선언합니다.

UPROPERTY()
UObject* Target;

이 순간 중요한 일이 발생합니다.

Reflection 시스템 등록
↓
GC 추적 가능
↓
직렬화 가능
↓
에디터 노출 가능

즉, UPROPERTY는 단순 매크로가 아닙니다.

👉 “이 멤버는 엔진이 관리해야 하는 UObject 참조”라고 선언하는 것입니다.


🧠 Reflection 시스템과 GC의 연결

이전 글에서 설명했던 Reflection 시스템이 여기서 다시 등장합니다.

언리얼은 UPROPERTY 정보를 기반으로

  • 현재 클래스가 어떤 UObject를 참조하는지
  • 어떤 멤버를 순회해야 하는지

알 수 있습니다.

개념적으로는 이런 느낌입니다.

UClass
 └── Property 목록 보유

Property
 └── UObject 포인터 정보 보유

GC는 이 정보를 이용해 객체 그래프를 순회합니다.

루트 UObject
↓
UPROPERTY 순회
↓
참조 UObject 발견
↓
계속 순회

형태입니다.


🎯 원시 포인터(raw pointer)가 위험한 이유

이제 왜 원시 포인터(raw pointer)가 위험한지 이해할 수 있습니다.

예를 들어

UObject* Target;

이렇게만 작성하면, GC는 이 포인터를 알 수 없습니다.

GC 입장:
"이 UObject를 아무도 참조하지 않는구나"

라고 판단할 수 있습니다.

결과적으로

  • GC가 객체 제거
  • 포인터는 여전히 이전 주소 유지
  • 댕글링 포인터(Dangling Pointer) 문제 발생

문제가 생길 수 있습니다.

즉, 포인터는 살아있는데 실제 UObject는 이미 제거되는 문제가 발생할 수 있습니다.


⚠️ 그래서 이런 코드가 위험합니다

class AMyActor : public AActor
{
public:
    UObject* Target;
};

겉보기에는 문제 없어 보이지만

  • GC가 추적하지 못하고
  • 객체가 제거될 수 있으며
  • Target은 죽은 객체 주소를 들고 있을 수 있습니다

그래서 언리얼 엔진에서는

UPROPERTY()
UObject* Target;

형태로 UPROPERTY() 매크로를 붙여서 UObject 타입의 변수를 선언합니다.


🧩 GC는 실제로 어떻게 순회할까?

언리얼 엔진의 가비지 컬렉터는 훨씬 더 복잡한 매커니즘으로 동작하지만,
매우 단순화하면 Mark & Sweep 방식에 가까운 동작 매커니즘을 갖습니다.

흐름은 대략 이렇습니다.


1. 루트 객체 찾기

엔진이 반드시 살아있다고 판단하는 객체들이 있습니다.

주로

  • World
  • GameInstance
  • Level
  • PlayerController

이 객체들이 시작점입니다.


2. 참조 순회

GC는 각 UObject의 UPROPERTY를 검사합니다.

Object
↓
UPROPERTY 목록 확인
↓
참조 UObject 발견
↓
재귀적으로 순회

3. Reachable 표시

도달 가능한 객체는 살아있는 것으로 표시합니다.

Reachable = true

4. Sweep 단계

끝까지 순회한 뒤에도 도달 불가능한 UObject는 제거 대상이 됩니다.

참조 없음
↓
GC 제거 가능

상태가 됩니다.


🎮 왜 std::shared_ptr를 안 사용할까?

언리얼 엔진을 사용하다보면 이 질문도 자주 등장합니다.

❓ 그냥 std::shared_ptr 쓰면 안 되나?

이 질문에 대한 답은 생각보다 단순하지 않습니다.
언리얼 엔진이 UObject에 대해 shared_ptr 기반 구조를 사용하지 않는 이유는 여러 가지가 있습니다.


1. 순환 참조 문제

shared_ptr는 순환 참조에 취약합니다.

A → B
B → A

서로 참조하면 참조 카운트가 0이 되지 않습니다.

게임 엔진에서는 객체 관계가 매우 복잡하기 때문에, 순환 참조 관리 비용이 커질 수 있습니다.


2. 엔진 전체와 통합되어야 함

언리얼 엔진은 단순하게 메모리 관리만 하는 것이 아닙니다.

언리얼 엔진의 GC는

  • Reflection
  • 에디터
  • 직렬화
  • 블루프린트
  • 네트워크 복제

와 모두 연결됩니다.

즉 UObject 시스템은 엔진 전체 메타 시스템과 연결된 객체 구조라고 할 수 있습니다.


3. UObject는 특별한 객체 시스템이다

언리얼에서 UObject는 일반 C++ 객체와 다릅니다.

예를 들어:

  • 이름(Name)
  • Outer
  • Class 정보
  • Reflection 정보
  • GC 정보

를 내부적으로 가질 수 있습니다.

즉, 단순 shared_ptr 기반 객체보다 훨씬 복잡한 엔진 객체입니다.


🧠 TWeakObjectPtr는 왜 필요할까?

GC 구조를 이해하면 이것도 자연스럽게 연결됩니다.

가끔은

  • 객체를 참조는 하고 싶지만
  • GC 생존에는 영향 주고 싶지 않은 경우

가 있습니다.

이때 사용하는 것이

TWeakObjectPtr

입니다.

TWeakObjectPtr은

  • 객체가 살아있는지 확인 가능
  • GC 제거 여부 감지 가능
  • Dangling Pointer 방지 가능

특징을 가집니다.

즉, GC 기반 UObject 환경에 맞춘 Weak Pointer라고 볼 수 있습니다.


🎯 핵심 정리

  • Unreal Engine은 UObject 계열 객체를 GC 기반으로 관리합니다.
  • GC는 Reachability(도달 가능성)를 기준으로 객체를 판단합니다.
  • UPROPERTY는 GC가 UObject 참조를 추적할 수 있도록 Reflection 시스템에 등록합니다.
  • raw pointer만 사용하면 GC가 참조를 알 수 없어 위험할 수 있습니다.
  • 언리얼 GC는 Reflection 시스템과 깊게 연결되어 있습니다.
  • TWeakObjectPtr는 GC 환경에서 안전한 약한 참조를 제공합니다.

🧩 마무리

처음 언리얼을 공부하면

  • 왜 UPROPERTY를 붙여야 하지?
  • 왜 raw pointer가 위험하지?
  • 왜 UObject는 특별하지?

같은 부분이 굉장히 낯설게 느껴질 수 있습니다.

하지만 내부 구조를 이해해보면, 이 모든 것이 Reflection + GC + 엔진 객체 시스템으로 연결되어 있다는 것을 알 수 있습니다.

즉 언리얼의 UObject는 단순 C++ 객체가 아니라

  • 엔진이 추적하고
  • 에디터가 이해하고
  • GC가 관리하는

특별한 객체 시스템입니다.

이 구조를 이해하면 UPROPERTY()가 단순 문법이 아니라, 엔진 전체 구조와 연결된 핵심 요소라는 점이 보이기 시작합니다.

👉 게임 엔진 구조를 더 깊이 이해하고 싶다면

아래 강의를 통해 직접 구현해보는 것을 추천드립니다.

C++로 만드는 게임 엔진 프레임워크 강의

👉 C++로 만드는 게임 엔진 프레임워크 강의 바로가기

//
   

댓글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다