본문 바로가기

AActor와 UObject의 차이점

UObject와 AActor의 수명·월드 배치·컴포넌트·복제 차이를 비교하고 생성 API를 올바르게 선택합니다.

인벤토리 관리자를 만든다고 해 봅시다. 이 관리자는 월드 어딘가에 놓여야 할까요, 아니면 위치 없이 데이터만 가지고 있으면 될까요? 언리얼 C++에서 클래스를 만들 때마다 이런 질문을 하게 되는데, 그 답에 따라 상속할 기본 클래스가 AActor와 UObject로 갈립니다. 잘못 고르면 쓸모없는 트랜스폼과 Tick을 가진 액터가 레벨에 쌓이거나, 반대로 월드에 놓여야 할 대상을 월드에 놓을 수 없게 됩니다.

C++ 클래스와 언리얼 리플렉션 시스템에서 리플렉션 매크로를 익혔으니, 이 글에서는 그 매크로가 붙는 객체 시스템의 두 기본 클래스를 비교합니다. 각 클래스가 무엇을 제공하는지, 어떤 경로로 만들고 어떻게 수명을 관리하는지, 그리고 언제 무엇을 고르는지를 차례로 살펴봅니다.


UObject: 언리얼 객체 시스템의 바탕

UObject는 언리얼의 리플렉션 객체 시스템을 이루는 기본 클래스입니다. 액터, 컴포넌트, 에셋, 게임 모드처럼 언리얼에서 객체라고 부르는 거의 모든 것이 직간접적으로 UObject를 상속합니다. 다만 일반 C++ 클래스나 값 타입 구조체까지 모두 UObject를 상속하는 것은 아닙니다.

UObject를 상속하면 엔진이 제공하는 여러 관리 기능에 참여할 수 있습니다. 첫째는 리플렉션 시스템입니다. UCLASS, UPROPERTY, UFUNCTION 매크로로 엔진에 등록되므로 에디터 노출, 블루프린트 접근, 직렬화에 참여할 수 있으며, 실제로 어디까지 허용되는지는 각 타입과 지정자에 따라 다릅니다.

둘째는 가비지 컬렉션(garbage collection, GC)입니다. UObject의 수명은 언리얼의 가비지 컬렉터가 관리합니다. GC는 루트 세트(root set)로 지정된 객체들에서 출발해 자신이 추적하는 강한 참조를 따라가는데, 이 경로로 도달할 수 없는 객체는 회수 대상이 됩니다. 이 글에서 '루트'라고 부르는 것이 이 출발점입니다. 그래서 아무 C++ 포인터에 주소를 담아 두는 것만으로는 객체가 살아 있다는 보장이 없습니다.

셋째는 직렬화(serialization)입니다. UObject는 자신과 속성을 저장하고 불러오는 기능을 가지고 있어, 에셋을 디스크에 저장하거나 게임 상태를 저장할 때 쓰입니다. 넷째는 에디터 통합으로, 에셋으로 만든 UObject 계열은 콘텐츠 브라우저에서 다룰 수 있습니다. 하지만 실행 중에 만든 일반 객체가 모두 에셋으로 보이는 것은 아니며, 디테일 패널의 편집도 타입과 지정자가 허용한 범위만큼만 됩니다.

마지막으로 UObject는 보통 자신이 들어 있는 다른 객체를 아우터(Outer)로 가집니다. NewObject로 객체를 만들 때 첫 인자로 아우터를 정할 수 있고, 생략하면 엔진의 임시 패키지(transient package)가 아우터가 됩니다. UObject의 이름은 같은 아우터 안에서 구분되고, 다른 UObject를 강한 참조로 가리키는 방법도 제공됩니다.

정리하면 UObject는 메모리 관리, 직렬화, 리플렉션 같은 엔진의 관리 기능을 받을 수 있는 가장 작은 단위의 클래스입니다. 월드에 배치하지 않더라도 객체 하나하나의 정체성과 엔진의 수명 관리가 필요하면 UObject 계열을 고를 수 있습니다. 반대로 단순한 값 데이터라면 USTRUCT나 일반 C++ 구조체가 더 알맞을 수 있습니다.


AActor: 월드에 놓이는 객체

AActor는 월드에 배치하거나 스폰하는 액터의 기본 클래스입니다. 캐릭터, 카메라, 라이트, 스폰 지점, 나무, 건물처럼 게임 화면에서 보거나 상호작용하는 대부분의 것이 AActor를 상속합니다. 다만 월드를 그리는 데 쓰이는 컴포넌트나 메시 에셋까지 모두 액터 인스턴스인 것은 아닙니다.

AActor는 UObject를 상속하므로 UObject의 기능을 그대로 물려받습니다. 액터도 가비지 컬렉션의 대상이고, 직렬화되며, 에디터에 노출됩니다. 여기에 UObject에는 없는 기능이 더해집니다.

먼저 트랜스폼입니다. 액터의 위치(Location), 회전(Rotation), 크기(Scale)는 루트 USceneComponent의 트랜스폼으로 표현되며, AActor가 이 값을 직접 저장하지는 않습니다. 다음으로 액터는 여러 UActorComponent를 붙여 기능을 넓히는 그릇입니다. 화면에 보이는 StaticMeshComponent, 충돌을 맡는 CapsuleComponent, 이동을 맡는 이동 컴포넌트처럼 역할을 나눠 조합하면 복잡한 액터도 구조적으로 관리할 수 있습니다.

액터에는 게임 월드에서 특정 시점에 불리는 수명 주기 함수도 있습니다. BeginPlay(), Tick(), EndPlay() 같은 함수를 재정의(override)해 액터의 동작을 정합니다. 또 멀티플레이어 게임에서는 액터가 네트워크를 통해 자신의 상태를 클라이언트에 복제(replication)하는 기본 단위가 됩니다. 충돌 감지와 물리 시뮬레이션에는 UPrimitiveComponent 계열 컴포넌트와 충돌, 물리 설정을 통해 참여합니다.


만들고 보관하는 방법

객체 종류에 맞는 생성과 수명 관리

NewObject 효과→도달 가능한 관리 객체의 ActiveEffect 보관→참조 비움에서 강한 경로와 회수 대상 가능성이 바뀐다. 즉시 해제·Actor Destroy 생존 연장으로 표시하지 않는다.

UNREAL C++ · CH2-2

객체 종류에 맞는 생성과 수명 관리

일반 UObject, 기본·동적 컴포넌트, 월드 액터의 생성 경로를 구분하고 생존·등록·부착을 별도로 확인합니다.

보관 중: 루트에서 관리 객체를 거쳐 효과에 도달합니다.

일반 UObject 효과의 강한 참조 retainedGC루트→도달가능관리객체의강한참조,관리객체의UPROPERTYActiveEffect→일반UObject효과.보관을비운상태는다른강한참조없음가정에서뒤간선만없으며회수대상가능≠즉시해제이다.액터Destroy의생존연장을뜻하지않는다.
GC 루트도달 가능한 경로
연결 관계GC 루트 · 도달 가능한 경로 → 관리 객체 · UPROPERTY · ActiveEffect
관리 객체UPROPERTYActiveEffect
참조참조관리 객체 · UPROPERTY · ActiveEffect → 일반 UObject 효과 · 생성된 객체
일반 UObject 효과생성된 객체
일반 UObject 효과의 강한 참조 retainedGC루트→도달가능관리객체의강한참조,관리객체의UPROPERTYActiveEffect→일반UObject효과.보관을비운상태는다른강한참조없음가정에서뒤간선만없으며회수대상가능≠즉시해제이다.액터Destroy의생존연장을뜻하지않는다.
GC 루트도달 가능한 경로
연결 관계GC 루트 · 도달 가능한 경로 → 관리 객체 · UPROPERTY · ActiveEffect
관리 객체UPROPERTYActiveEffect
강한 참조강한 참조관리 객체 · UPROPERTY · ActiveEffect → 일반 UObject 효과 · 생성된 객체
일반 UObject 효과생성된 객체

참조를 비운 뒤: 다른 강한 참조가 없다면 일반 효과는 회수 대상이 될 수 있습니다.

일반 UObject 효과의 강한 참조 releasedGC루트→도달가능관리객체의강한참조,관리객체의UPROPERTYActiveEffect→일반UObject효과.보관을비운상태는다른강한참조없음가정에서뒤간선만없으며회수대상가능≠즉시해제이다.액터Destroy의생존연장을뜻하지않는다.GC 루트도달 가능한 경로관리 객체UPROPERTYActiveEffect일반 UObject 효과생성된 객체ActiveEffect = nullptr다른 강한 참조 없음회수 대상 가능 ≠ 즉시 해제
일반 UObject 효과의 강한 참조 releasedGC루트→도달가능관리객체의강한참조,관리객체의UPROPERTYActiveEffect→일반UObject효과.보관을비운상태는다른강한참조없음가정에서뒤간선만없으며회수대상가능≠즉시해제이다.액터Destroy의생존연장을뜻하지않는다.GC 루트도달 가능한 경로관리 객체UPROPERTYActiveEffect일반 UObject 효과생성된 객체ActiveEffect = nullptr다른 강한 참조 없음회수 대상 가능 ≠ 즉시 해제
생성하려는 대상과 수명 관리
생성 상황생성·등록 경로수명에서 확인할 점
일반 객체NewObject<T>(Outer)GC가 추적하는 강한 참조를 유지할 위치
기본 컴포넌트생성자에서 CreateDefaultSubobject<T>()액터의 기본 구성; 씬 컴포넌트 부착 관계는 별도 설정
동적 컴포넌트NewObject<T>(Owner) 후 RegisterComponent()등록과 공간 부착은 별개; 소유 액터와 정리 시점 확인
런타임 액터월드의 SpawnActor<T>()Destroy 후 IsValid 등으로 유효성 확인; 직접 delete하지 않음
일반 객체
생성·등록 경로: NewObject<T>(Outer)
수명에서 확인할 점: GC가 추적하는 강한 참조를 유지할 위치
기본 컴포넌트
생성·등록 경로: 생성자에서 CreateDefaultSubobject<T>()
수명에서 확인할 점: 액터의 기본 구성; 씬 컴포넌트 부착 관계는 별도 설정
동적 컴포넌트
생성·등록 경로: NewObject<T>(Owner) 후 RegisterComponent()
수명에서 확인할 점: 등록과 공간 부착은 별개; 소유 액터와 정리 시점 확인
런타임 액터
생성·등록 경로: 월드의 SpawnActor<T>()
수명에서 확인할 점: Destroy 후 IsValid 등으로 유효성 확인; 직접 delete하지 않음

Outer나 포인터 변수만으로 생존이 보장되지는 않습니다. GC가 추적할 강한 참조와 소유 객체의 도달 가능성을 확인합니다. Tick·복제·충돌도 별도 설정입니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명GC 루트→도달 가능한 관리 객체의 강한 참조, 관리 객체의 UPROPERTY ActiveEffect→일반 UObject 효과로 이어진다. 보관을 비운 상태는 다른 강한 참조가 없다는 가정에서 뒤 간선만 없으며 회수 대상 가능≠즉시 해제다. 액터 Destroy의 생존 연장을 뜻하지 않는다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
효과 생성

NewObject로 효과 UObject를 만든다.

  • 객체: 효과 UObject
  • 조건: 유효한 소유/참조 경로
  • 일반 C++ new와 UObject 관리의 관계를 구분한다.
강한 보관

도달 가능한 관리 객체의 ActiveEffect가 효과를 보관한다.

  • 필드: ActiveEffect
  • 상태: 강한 도달 경로 유지
  • 필드가 있는 소유자도 루트에서 도달 가능해야 한다.
참조 비움

필드를 비워 다른 강한 경로 유무가 중요해진다.

  • ActiveEffect: 비움
  • GC: 회수 대상 가능
  • 즉시 해제 시점이나 Actor Destroy 수명 연장을 보장하지 않는다.

객체 종류에 맞는 생성과 수명 관리

NewObject 효과→도달 가능한 관리 객체의 ActiveEffect 보관→참조 비움에서 강한 경로와 회수 대상 가능성이 바뀐다. 즉시 해제·Actor Destroy 생존 연장으로 표시하지 않는다.

UNREAL C++ · CH2-2

객체 종류에 맞는 생성과 수명 관리

일반 UObject, 기본·동적 컴포넌트, 월드 액터의 생성 경로를 구분하고 생존·등록·부착을 별도로 확인합니다.

보관 중: 루트에서 관리 객체를 거쳐 효과에 도달합니다.

일반 UObject 효과의 강한 참조 retainedGC루트→도달가능관리객체의강한참조,관리객체의UPROPERTYActiveEffect→일반UObject효과.보관을비운상태는다른강한참조없음가정에서뒤간선만없으며회수대상가능≠즉시해제이다.액터Destroy의생존연장을뜻하지않는다.
GC 루트도달 가능한 경로
연결 관계GC 루트 · 도달 가능한 경로 → 관리 객체 · UPROPERTY · ActiveEffect
관리 객체UPROPERTYActiveEffect
참조참조관리 객체 · UPROPERTY · ActiveEffect → 일반 UObject 효과 · 생성된 객체
일반 UObject 효과생성된 객체
일반 UObject 효과의 강한 참조 retainedGC루트→도달가능관리객체의강한참조,관리객체의UPROPERTYActiveEffect→일반UObject효과.보관을비운상태는다른강한참조없음가정에서뒤간선만없으며회수대상가능≠즉시해제이다.액터Destroy의생존연장을뜻하지않는다.
GC 루트도달 가능한 경로
연결 관계GC 루트 · 도달 가능한 경로 → 관리 객체 · UPROPERTY · ActiveEffect
관리 객체UPROPERTYActiveEffect
강한 참조강한 참조관리 객체 · UPROPERTY · ActiveEffect → 일반 UObject 효과 · 생성된 객체
일반 UObject 효과생성된 객체

참조를 비운 뒤: 다른 강한 참조가 없다면 일반 효과는 회수 대상이 될 수 있습니다.

일반 UObject 효과의 강한 참조 releasedGC루트→도달가능관리객체의강한참조,관리객체의UPROPERTYActiveEffect→일반UObject효과.보관을비운상태는다른강한참조없음가정에서뒤간선만없으며회수대상가능≠즉시해제이다.액터Destroy의생존연장을뜻하지않는다.GC 루트도달 가능한 경로관리 객체UPROPERTYActiveEffect일반 UObject 효과생성된 객체ActiveEffect = nullptr다른 강한 참조 없음회수 대상 가능 ≠ 즉시 해제
일반 UObject 효과의 강한 참조 releasedGC루트→도달가능관리객체의강한참조,관리객체의UPROPERTYActiveEffect→일반UObject효과.보관을비운상태는다른강한참조없음가정에서뒤간선만없으며회수대상가능≠즉시해제이다.액터Destroy의생존연장을뜻하지않는다.GC 루트도달 가능한 경로관리 객체UPROPERTYActiveEffect일반 UObject 효과생성된 객체ActiveEffect = nullptr다른 강한 참조 없음회수 대상 가능 ≠ 즉시 해제
생성하려는 대상과 수명 관리
생성 상황생성·등록 경로수명에서 확인할 점
일반 객체NewObject<T>(Outer)GC가 추적하는 강한 참조를 유지할 위치
기본 컴포넌트생성자에서 CreateDefaultSubobject<T>()액터의 기본 구성; 씬 컴포넌트 부착 관계는 별도 설정
동적 컴포넌트NewObject<T>(Owner) 후 RegisterComponent()등록과 공간 부착은 별개; 소유 액터와 정리 시점 확인
런타임 액터월드의 SpawnActor<T>()Destroy 후 IsValid 등으로 유효성 확인; 직접 delete하지 않음
일반 객체
생성·등록 경로: NewObject<T>(Outer)
수명에서 확인할 점: GC가 추적하는 강한 참조를 유지할 위치
기본 컴포넌트
생성·등록 경로: 생성자에서 CreateDefaultSubobject<T>()
수명에서 확인할 점: 액터의 기본 구성; 씬 컴포넌트 부착 관계는 별도 설정
동적 컴포넌트
생성·등록 경로: NewObject<T>(Owner) 후 RegisterComponent()
수명에서 확인할 점: 등록과 공간 부착은 별개; 소유 액터와 정리 시점 확인
런타임 액터
생성·등록 경로: 월드의 SpawnActor<T>()
수명에서 확인할 점: Destroy 후 IsValid 등으로 유효성 확인; 직접 delete하지 않음

Outer나 포인터 변수만으로 생존이 보장되지는 않습니다. GC가 추적할 강한 참조와 소유 객체의 도달 가능성을 확인합니다. Tick·복제·충돌도 별도 설정입니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.

관계도의 보충 설명

원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명GC 루트→도달 가능한 관리 객체의 강한 참조, 관리 객체의 UPROPERTY ActiveEffect→일반 UObject 효과로 이어진다. 보관을 비운 상태는 다른 강한 참조가 없다는 가정에서 뒤 간선만 없으며 회수 대상 가능≠즉시 해제다. 액터 Destroy의 생존 연장을 뜻하지 않는다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
효과 생성

NewObject로 효과 UObject를 만든다.

  • 객체: 효과 UObject
  • 조건: 유효한 소유/참조 경로
  • 일반 C++ new와 UObject 관리의 관계를 구분한다.
강한 보관

도달 가능한 관리 객체의 ActiveEffect가 효과를 보관한다.

  • 필드: ActiveEffect
  • 상태: 강한 도달 경로 유지
  • 필드가 있는 소유자도 루트에서 도달 가능해야 한다.
참조 비움

필드를 비워 다른 강한 경로 유무가 중요해진다.

  • ActiveEffect: 비움
  • GC: 회수 대상 가능
  • 즉시 해제 시점이나 Actor Destroy 수명 연장을 보장하지 않는다.

위 그림의 위쪽은 일반 UObject 효과가 GC에서 살아남는 조건을 보여 줍니다. 보관 중인 상태에서는 GC 루트에서 도달할 수 있는 관리 객체가 있고, 그 관리 객체의 UPROPERTY 멤버 ActiveEffect가 생성된 효과 객체를 강한 참조로 가리킵니다. 그래서 루트에서 관리 객체를 거쳐 효과까지 경로가 이어집니다. 참조를 비운 뒤의 그림에서는 ActiveEffect = nullptr로 마지막 간선이 사라지는데, 다른 강한 참조가 없다면 효과는 회수 대상이 될 수 있습니다. 그림에 적힌 것처럼 회수 대상이 된다는 것이 즉시 해제된다는 뜻은 아닙니다.

그림의 아래쪽 표는 만들려는 대상마다 생성 경로와 수명에서 확인할 점을 정리합니다. 일반 객체는 NewObject<T>(Outer)로 만들고, GC가 추적할 강한 참조를 어디에 둘지 정해야 합니다. 액터의 기본 컴포넌트는 생성자에서 CreateDefaultSubobject<T>()로 만들며, 씬 컴포넌트의 부착 관계는 따로 설정합니다. 실행 중에 추가하는 동적 컴포넌트는 NewObject<T>(Owner)로 만든 뒤 RegisterComponent()로 등록하는데, 등록과 공간 부착은 별개의 일이므로 소유 액터와 정리 시점을 함께 확인합니다. 런타임 액터는 월드의 SpawnActor<T>()로 만들고, Destroy 뒤에는 IsValid 같은 함수로 유효성을 확인하며, 직접 delete하지 않습니다.

단계 그림은 효과 하나의 수명을 세 단계로 따라갑니다. 먼저 NewObject로 효과를 만들고, 다음으로 도달 가능한 관리 객체의 ActiveEffect가 그 효과를 보관하며, 마지막으로 그 필드를 비우면 다른 강한 경로가 있는지가 회수 여부를 가릅니다. 여기서 놓치기 쉬운 점은 필드를 가진 관리 객체 자신도 루트에서 도달할 수 있어야 한다는 것입니다.

그래서 계속 쓸 UObject 참조를 멤버에 둘 때는, 도달 가능한 객체 안에 UPROPERTY() TObjectPtr<T>처럼 GC가 추적하는 강한 참조로 둡니다. UPROPERTY() 없이 TObjectPtr만 쓰거나 아우터만 지정하는 것은 같은 효과를 내지 않습니다. 뒤의 예제에는 UPROPERTY()를 붙인 UStaticMeshComponent* 같은 원시 포인터 멤버도 나오는데, 게임 프로젝트의 코드에서는 UHT가 기본 설정으로 이런 멤버를 허용하므로 이것도 GC가 추적하는 강한 참조가 됩니다. 다만 Epic은 가능하면 이런 멤버를 UPROPERTY() TObjectPtr<T>로 바꾸라고 권합니다.

앞 글의 ActiveEffect를 가진 관리 객체가 GC 루트에서 도달 가능하다고 하고, 구체적으로 따라가 봅시다. NewObject로 만든 일반 UObject 효과를 그 멤버에 넣으면, 관리 객체에서 효과로 이어지는 추적 가능한 강한 참조가 생깁니다. 다른 강한 참조가 전혀 없는 상태에서 ActiveEffect를 비우면 효과는 GC의 회수 대상이 될 수 있지만, 대입한 직후 바로 메모리가 회수되는 것은 아닙니다. 같은 객체를 원시 포인터 지역 변수에 담아 두었거나 아우터를 정해 두었다고 해서, 그것이 강한 참조를 대신하지는 않습니다.

이 예는 일반 UObject의 GC 참조에 관한 것입니다. 액터는 사정이 다릅니다. Destroy를 호출했거나 월드가 끝나서 더 이상 게임플레이에 쓸 수 없게 된 액터를, 강한 참조를 쥐고 있다는 이유로 계속 살려 쓸 수 있는 것은 아닙니다. 그래서 액터를 가리키는 참조는 쓸 때마다 유효한지 다시 확인합니다.


자주 하는 오해

UObject도 월드 위치를 가진다고 생각하기 쉽지만 그렇지 않습니다. UObject에는 트랜스폼이 없으므로, 월드 트랜스폼이 필요한 대상은 AActor나 USceneComponent를 바탕으로 설계해야 합니다.

액터를 delete로 지우는 것도 흔한 실수입니다. 액터와 UObject는 언리얼의 수명 주기에 따라 Destroy와 GC로 정리되며, 직접 delete하면 엔진이 아직 그 객체를 가리키는 상태에서 메모리가 사라져 문제가 생깁니다.

복제가 아무 클래스에서나 자동으로 동작한다고 생각하는 경우도 있습니다. 네트워크 복제는 기본적으로 AActor와 그 컴포넌트를 통해 설계하는 것이 표준이며, 8장에서 자세히 다룹니다.

게임 인스턴스나 월드의 수명에 맞춰 존재하는 관리자가 필요하다면 서브시스템(Subsystem) 계열도 검토합니다. 서브시스템은 액터 배치를 대신하는 타입이 아니라, 엔진이 정해진 수명에 맞춰 만들고 정리해 주는 UObject 계열입니다.


언제 무엇을 고를까

AActor를 상속하는 것이 알맞은 경우는 다음과 같습니다.

  • 캐릭터, 건물, 무기, 차량, 조명처럼 게임 월드에 보이게 배치되어야 하는 대상
  • 3D 공간에서 위치, 회전, 스케일을 가져야 하는 대상
  • 여러 컴포넌트를 붙여 복합적인 기능을 해야 하는 대상
  • 충돌이나 트리거 같은 게임 이벤트에 반응해야 하는 대상

UObject를 상속하는 것이 알맞은 경우는 다음과 같습니다.

  • 월드에 직접 배치할 필요는 없지만 엔진의 가비지 컬렉션이나 직렬화가 필요한 데이터 객체
  • 인벤토리 관리자나 퀘스트 관리자 같은 특정 시스템의 관리자 클래스
  • UI 위젯이 보여 줄 데이터 모델
  • 값 구조체로는 부족하고 객체 참조가 필요한 블루프린트 데이터
  • UActorComponent를 상속하지 않는 순수한 논리 단위

더 깊이: 생성자는 클래스 기본 객체를 만들 때도 실행된다

언리얼은 UCLASS마다 클래스 기본 객체(Class Default Object, CDO)라는 객체를 하나씩 둡니다. CDO는 클래스 생성자가 만든 기본값의 틀이며, 만들어진 뒤에는 바뀌지 않습니다. 엔진은 시작할 때 C++ UObject 클래스들을 초기화하면서 각 클래스의 기본 생성자를 호출합니다. 그래서 생성자는 NewObject나 SpawnActor로 인스턴스를 만들 때만 실행되는 코드가 아닙니다. UObject가 생성자 인자를 받지 못하고, 기본 생성자가 없으면 컴파일되지 않는 것도 이 방식 때문입니다.

이 점을 알면 생성자에 무엇을 써야 하는지가 분명해집니다. 공식 문서는 UObject 생성자를 가볍게 유지해 기본값과 하위 객체를 정하는 데만 쓰고, 지역 데이터나 정적 에셋만 다루라고 합니다. 월드에서 다른 액터를 찾거나 게임 상태에 따라 값을 고르는 초기화는 생성자가 아니라 BeginPlay()에 둡니다. 생성자에서 그런 일을 하면 CDO를 만드는 시점처럼 월드가 없는 상황에서도 그 코드가 실행됩니다.

생성자가 실행되기 전에 UObject의 멤버는 엔진이 0으로 초기화해 둡니다. 그래서 생성자에서는 0이나 nullptr, false가 아닌 기본값만 정해도 됩니다. 다만 이 초기화는 UObject에만 해당하므로, UObject가 아닌 일반 C++ 클래스나 지역 변수는 C++의 규칙대로 직접 초기화해야 합니다.


정리

  • UObject는 리플렉션, 가비지 컬렉션, 직렬화, 에디터 통합을 제공하는 언리얼 객체 시스템의 기본 클래스이며, 단순한 값 데이터에는 구조체가 더 알맞을 수 있습니다.
  • AActor는 UObject를 상속하고, 월드 배치, 루트 컴포넌트의 트랜스폼, 컴포넌트 조합, 수명 주기 함수, 네트워크 복제를 더합니다.
  • 일반 객체는 NewObject, 기본 컴포넌트는 CreateDefaultSubobject, 동적 컴포넌트는 NewObject 뒤 RegisterComponent, 런타임 액터는 SpawnActor로 만듭니다.
  • 계속 쓸 UObject는 도달 가능한 객체 안의 UPROPERTY() TObjectPtr<T> 같은 강한 참조로 보관해야 하며, UPROPERTY()가 없는 원시 포인터나 아우터 지정만으로는 생존이 보장되지 않습니다.
  • 회수 대상이 되는 것과 즉시 해제되는 것은 다르고, 액터는 Destroy 뒤에 강한 참조로 살려 쓸 수 없으므로 유효성을 다시 확인합니다.

연습 문제

1. 생성 경로 고르기

월드를 날아가는 발사체 액터 하나와, 월드 위치 없이 객체 정체성만 필요한 효과 데이터 객체 하나를 실행 중에 만들려고 합니다. 각각 월드의 SpawnActor와 일반 객체의 NewObject 가운데 어느 경로를 고를까요?

해설 보기

발사체는 월드에 놓이고 움직여야 하는 액터이므로 월드의 액터 생성 경로인 SpawnActor를 씁니다. 효과 데이터는 위치가 필요 없는 일반 UObject이므로 NewObject로 만듭니다.

액터를 NewObject로 만들면 월드에 등록되지 않으므로 배치나 Tick 같은 액터의 기능이 동작하지 않습니다. 생성 경로는 대상이 월드에 속하는지에 따라 고릅니다.

2. 아우터만 정한 효과

앞 문제의 효과 데이터를 만들 때 아우터를 관리자 객체로 정했지만, 관리자 안에 그 효과를 가리키는 UPROPERTY 멤버는 두지 않았습니다. 이 효과가 오래 살아 있다고 기대할 수 있을까요?

해설 보기

기대할 수 없습니다. 아우터를 지정하는 것은 도달 가능한 소유 객체가 강한 UPROPERTY 참조로 효과를 보관하는 것과 같지 않습니다. 다른 강한 참조가 없다면 효과는 GC의 회수 대상이 될 수 있습니다.

계속 쓸 효과라면 관리자에 UPROPERTY() TObjectPtr 멤버를 두어 참조 경로를 유지합니다. 발사체 같은 액터는 이와 별개로 Destroy와 월드 수명에 따라 사라지므로, 쓸 때마다 유효성을 다시 확인합니다.

UPROPERTY로 표시한 TObjectPtr가 GC에 주는 영향과 원시 포인터, 약한 참조의 차이는 Epic의 Object Pointers 문서에서, 일반 객체를 만드는 방법은 Creating Objects 문서에서 확인할 수 있습니다.

다음 글 C++로 Blueprint 클래스 확장에서는 AActor를 상속하는 C++ 클래스를 만들고, 그 클래스를 부모로 하는 블루프린트로 기능을 넓혀 봅니다.