게임 개발/학습 일지

언리얼 엔진 심화 1주차 - 리플렉션/UHT/UBT/CDO

이개 2026. 4. 10. 16:12

시작하기 전에 - 어떤 코드가 좋은 코드인가

  1. 변수명 직관적으로 정하기
  2. 중복 코드 피하기
  3. 긴 함수 지양하기

 

리플렉션이란?

실행 중에 프로그램이 자기 자신의 구조를 인지하고 조작할 수 있는 능력. 메타데이터로 블루프린트와 C++ 코드 사이를 연결해준다.

 

리플렉션 매크로

  • UCLASS: 클래스를 언리얼에 등록
  • UPROPERTY: 변수를 에디터에 노출
  • UFUNCTION: 함수를 블루프린트에 연결

프로퍼티 지정자 목록

https://dev.epicgames.com/documentation/unreal-engine/property-specifiers?application_version=4.27

 

Property Specifiers | Unreal Engine 4.27 Documentation | Epic Developer Community

Keywords used when declaring UProperties to specify how the property behaves with various aspects of the Engine and Editor.

dev.epicgames.com

 

리플렉션의 주요 역할

  • 런타임 타입 정보 확인
    • IsA() 함수로 객체 타입 실시간 확인
  • 에디터 연동 
    • UPROPERTY 변수가 디테일 창에 자동 표시
  • 가비지 컬렉션 지원
    • 표준 C++: new/delete로 직접 메모리 관리
      • 메모리 누수 위험
      • dangling 포인터 문제
    • 언리얼 엔진: GC가 자동 관리
      • 안전한 메모리 해제
      • 개발자 실수 방지
    • 가비지 컬렉션이 있지만 언리얼에서도 스마트 포인터 역할을 하는 TSharedPtr이 따로 있음
      • 가비지 컬렉션은 UObject를 상속받은 클래스만 관리할 수 있음. 
      • UObject가 아닌 것을 관리하기 위해 존재.

 

가비지 컬렉션

동작 원리

언리얼에서 가비지 컬렉션은 Root에서 시작하여 Mark and Sweep 알고리즘을 사용하여 깊이 우선 탐색으로 다른 객체들을 순회하며 마크하고, 마지막에 마크되지 않은 객체들을 삭제한다.

 

GC 실행

// GC는 상당한 비용이 듦 (모든 객체 순회)
FGCCollectionTag CollectionTag = FGCCollectionTag::GCEditor;  // 에디터에서만 GC
GetWorld()->ForceGarbageCollection(false);  // 강제 GC 실행

// 게임 중에는 자동으로 주기적으로 실행됨
// (설정으로 조절 가능)

 

  • 언리얼 C++의 GC 특징
    • 수동으로 Root 관리 가능
    • UPROPERTY() 매크로로 자동 참조 추적
    • 약한 참조(TWeakObjectPtr)로 순환 참조 방지
UCLASS()
class AMyActor : public AActor
{
    UPROPERTY()
    class UMyComponent* Component;  // 자동 추적
    
    TWeakObjectPtr<UMyObject> WeakRef;  // GC 영향 없음
};

Root Set에 등록되지 않은 UObject는 모두 GC 수거 대상이 되므로, NewObject()로 만든 동적 객체는 필요에 따라 AddToRoot()로 보호하거나 UPROPERTY로 참조를 유지해야 한다.

 

Root Set

GC가 수행될 때 절대 삭제되지 않아야 할 객체들의 집합.

  • Root에 등록되는 경우
    • 월드 객체들
      • 현재 로드된 World와 그 안의 모든 Actor들
      • World -> Actors -> Components 순으로 참조
    • 게임 상태 관련 객체들
      • AGameState
      • APlayerState
      • Acontroller
      • APawn
    • 클래스 기본값(CDO - Class Default Object)
      • 각 클래스마다 하나씩 존재하는 기본값
        • ACharaceter::StaticClass()->GetDefaultObject()
    • 명시적으로 AddToRoot() 호출
      • MyObject ->AddToRoot() // Root에 직접 등록
      • MyObject->RemoveFromRoot() // GC 대상이 됨
    • GEngine, GWorld 등의 전역 객체
      • 엔진 레벨의 전역 시스템들
// 예시
void MyGameMode::BeginPlay()
{
    // 1. AActor 생성 시 자동으로 World에 추가
    AMyCharacter* Character = GetWorld()->SpawnActor<AMyCharacter>();
    
    // 2. World가 Root Set에 있으므로
    //    Character도 자동으로 보호됨
    
    // 3. 명시적으로 Root에 등록해야 하는 경우
    UMyDataAsset* DataAsset = NewObject<UMyDataAsset>();
    DataAsset->AddToRoot();  // 이제 GC 대상이 아님
}

void MyGameMode::EndPlay()
{
    // NewObject로 만든 객체 정리
    if (DataAsset)
    {
        DataAsset->RemoveFromRoot();
        DataAsset = nullptr;  // GC에서 수거됨
    }
}

 

// ❌ 위험: GC가 참조를 모름
UMyObject* Obj = NewObject<UMyObject>();
SomeVariable = Obj;  // 일반 포인터는 GC가 추적 불가
// → GC 실행 시 Obj 삭제됨 (포인터 대롱만 남음)

// ✅ 안전: GC가 자동 추적
UPROPERTY()
UMyObject* Obj;  // GC가 참조 추적
// → 이 Actor가 Root에 있으면 Obj도 자동 보호됨

// ✅ 안전: 명시적 보호
UMyObject* Obj = NewObject<UMyObject>();
Obj->AddToRoot();  // Root에 직접 등록
// → GC에서 절대 삭제 안 됨

 

가비지 컬렉션 테스트하기

Project Settings-> Engine -> Garbage Collection에서 가비지 컬렉션 주기 세팅. 10으로 세팅하고 테스트한다.

 

All Classes-> Object로 C++ 클래스 생성

 

헤더 만들고 Alt+ Enter 누르면 선언부에 틀이 생성된다.

 

 

Super:: 는 수작업으로 작성해줘야하는 것일까?

Rider를 사용하거나 Reshaper 플러그인을 설치하여 Alt + Insert -> Override members를 선택하는 수밖에 없다고 한다.

 

 

C++ Actor 클래스를 하나 만든데

 

 

옵저버 클래스를 만들고, UPROPERTY() 매크로가 적용된 포인터와 아닌 포인터 두 개를 선언해준다.

 

언리얼에서는 UObject와 Actor는 동적 할당하는 방법이 다르다.

 

MyTestObject.h를 인클루드하지 않으면 이렇게 나온다.
MyGCObserver 오브젝트를 뷰포트에 배치하여 가비지 컬렉션 작동을 확인한다.

 

시작 시 MYGCObeserver 생성자에 작성한 가비지 컬렉션 작동 로그가 뜬다

 

UPROPERTY가 아닌 DangerObject는 10초 뒤 가비지 컬렉션 실행 타임에 사라진다.

 

UPROPERTY인 SafeObject는 플레이 종료 시에 사라진다.

 

 

한글 인코딩이 깨져서 안 나올 경우, C++ 파일을 인코딩하여 저장해주면 된다.

인코딩하여 저장 누르고 덮어쓰기한다.
UTF-8로 저장하기

 

 

UPROPERTY 안 적어줘서 GC된 객체는 추적 불가능

// Called every frame
void AMyGCObserver::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	bool bSafeIf = SafeObject != nullptr;
	bool bSafeValid = IsValid(SafeObject);

	bool bDangerIf = DangerObject != nullptr;
	bool bDangerValid = IsValid(DangerObject);

	// key는 중복되면 덮어쓰기 된다.
	// 0.0f 넣으면 매번 호출되어 한 줄로 계속 떠 있다.
	GEngine->AddOnScreenDebugMessage(-1, 0.0f, FColor::Green, FString::Printf(TEXT("Safe(UPROPERTY) -> if : %s, IsValid : %s"), bSafeIf ? TEXT("True") : TEXT("false"), bSafeValid ? TEXT("True") : TEXT("false")));
	GEngine->AddOnScreenDebugMessage(-1, 0.0f, FColor::Red, FString::Printf(TEXT("Danger(UPROPERTY) -> if : %s, IsValid : %s"), bDangerIf ? TEXT("True") : TEXT("false"), bDangerValid ? TEXT("True") : TEXT("false")));

}

 

  • GEngine
    • 전역 엔진 객체
    • 타입: UEngine*
    • 언리얼이 실행되면 자동 생성됨
    • 어디서든 접근 가능
    • 현재 실행 중인 언리얼 엔진 그 자체

  • AddOnScreenDebugMessage
    • 화면 왼쪽 위에 텍스트 띄우는 함수
    • AddOnScreenDebugMessage(Key, Time, Color, Message)
      • key : 메시지 ID(매번 새로 추가)
      • Time: 출력 시간(0이면 한 프레임만)
      • Color: 글자색
      • Message: 출력 내용

 

DangerObject는 이미 사라졌는데, 체크는 계속 True가 된다.

 

❌ UPROPERTY 안 붙이는 경우

1. 단순한 지역 변수 (Local 변수)

 
void Func()
{
int a = 10;
}
 

👉 함수 안에서만 쓰는 변수
→ 엔진이 알 필요 없음


2. 일반 C++ 타입 (엔진 기능 필요 없음)

 
int Score;
float Speed;
bool bFlag;
 

👉 이런 건 그냥 써도 됨
(에디터 노출 / 저장 / 네트워크 필요 없으면)


3. UObject가 아닌 포인터

 
int* Ptr;
 

👉 GC랑 관련 없음 → 필요 없음


4. 임시 계산용 변수

 
float TempValue = A + B;
 

👉 프레임 내 계산용 → 굳이 추적할 필요 없음


5. GC에 영향 주면 안 되는 경우 (중요)

 
TWeakObjectPtr<AActor> Target;
 

👉 일부러 약한 참조로 쓰는 경우
→ UPROPERTY 없이도 사용 가능 (상황에 따라 붙이기도 함)


6. STL 컨테이너

 
std::vector<int> Data;
 

👉 Unreal은 TArray를 추적함
→ std::vector는 UPROPERTY로 관리 안 됨


⚠️ 반드시 붙여야 하는 경우 (더 중요)

이건 반대로 안 붙이면 큰일 나는 경우 👇


🚨 1. UObject 포인터 (핵심)

 
AActor* MyActor; // ❌ 위험
 

👉 GC가 이걸 모름 → 갑자기 삭제됨 (댕글링 포인터)

✔️ 반드시:

 
UPROPERTY()
TObjectPtr<AActor> MyActor;
 

🚨 2. 블루프린트에서 써야 할 때

 
UPROPERTY(EditAnywhere)
float Speed;
 

🚨 3. 저장 / 로딩 / 복제 필요

  • SaveGame
  • Replication
  • Editor 노출

 

 

위크 포인터 사용 예시: 적 유닛을 저장할 때, 적 유닛이 죽어서 없어질 수도 있어서 위크 포인터로 저장하면 됨

 

UBT

가장 먼저 실행되는 빌드 도구

전체 프로젝트 구조 파악

플랫폼별 환경 설정

 

주요 기능들

  • 플러그인 및 모듈 검색
  • .Target.cs 파일 분석
  • .Build.cs 파일 처리
  • 플랫폼 호환성 확인
  • 개발 환경 자동구성

Build.cs 파일. 어떤 프로그램이나 모듈이 사용되는지 읽어 컴퓨터에 맞춘 개발 환경을 세팅한다.

 

 

UHT

코드를 에디터가 이해할 수 있게 번역해준다.

UCLASS, UPROPERTY, UFUNCTION 등의 매크로가 붙은 클래스, 함수, 변수들 메타데이터 수집한다.

MyObject.generated.h 파일 자동 생성을 해준다.

에디터 디테일 창 연동 정보 생성 및 블루프린트 연결 정보 준비를 하여 리플렉션을 가능케 한다.

 

cpp파일이 바뀌면 라이브코딩으로 처리, 헤더 파일이 바뀌면 UHT로 수정해야 되기 때문에 껐다가 빌드를 다시 해줘야한다.

 

 

CDO(Class Default Object)

UObject 클래스마다 하나씩 존재하는 기본값 보관소. 모듈 로딩 단계에서 생성된다.

UBT → UHT → 컴파일 →엔진 초기화(CDO 생성)

생성자를 딱 한번만 호출한다.

 

// MyActor.cpp 파일

AMyActor::AMyActor()
{
    // 여기서 설정한 값은 CDO에 기록된다.
    MoveSpeed = 600.0f;
    bCanDash = true;
    MaxHealth = 100;
}

 

생성자 안에 적어 놓은 초기화 로직으로 CDO라는 원본 객체를 완성한다. 이후에는 생성자를 다시 호출할 필요가 없이 복사만 진행하면 된다.

 

 

최적화 원리

  • 메모리 절약: 공통 기본값을 CDO 하나에만 저장
  • 빠른 초기화: 런타임 체크 즉각 가능
  • 델타 직렬화: 차이점만 기록, 전송하기 때문에 파일 용량/네트워크 효율을 극대화한다.

 

CDO값 변경 예시

// Fill out your copyright notice in the Description page of Project Settings.


#include "MyGameModeBase.h"
#include "TestMyActor.h"

void AMyGameModeBase::BeginPlay()
{
	Super::BeginPlay();

	ATestMyActor* ActorA = GetWorld()->SpawnActor<ATestMyActor>();
	UE_LOG(LogTemp, Warning, TEXT("ActorA Init Health : %d"), ActorA->Health);

	ActorA->Health = 50;

	UE_LOG(LogTemp, Warning, TEXT("ActorA Modi : %d"), ActorA->Health);

	ATestMyActor* MyActorCDO = GetMutableDefault<ATestMyActor>();
	UE_LOG(LogTemp, Warning, TEXT("CDO Init Health : %d"), MyActorCDO->Health);

	// CDO 수정한 값
	MyActorCDO->Health = 200;
	UE_LOG(LogTemp, Warning, TEXT("CDO Modi Health : %d"), MyActorCDO->Health);

	ATestMyActor* ActorB = GetWorld()->SpawnActor<ATestMyActor>();
	UE_LOG(LogTemp, Warning, TEXT("ActorB Init Health : %d"), ActorB->Health);

	UE_LOG(LogTemp, Warning, TEXT("ActorA Current HP : %d"), ActorA->Health);



}
  • 첫 번째 실행
    • ActorA Init Health : 100
    • ActorA Modi : 50
    • CDO Init Health : 100
    • CDO Modi Health : 200
    • ActorB Init Health : 100
    • ActorA Current HP : 50
  • 두 번째 실행
    • ActorA Init Health : 100
    • ActorA Modi : 50
    • CDO Init Health : 200
    • CDO Modi Health : 200
    • ActorB Init Health : 100
    • ActorA Current HP : 50
 
ATestMyActor* MyActorCDO = GetMutableDefault<ATestMyActor>();
 

이 함수는 해당 클래스의 CDO(Class Default Object)를 “수정 가능하게” 가져오는 함수

GetMutableDefault()로 바꾼 CDO는 프로세스 메모리에 유지됨
상황CDO 값
첫 실행 전 100
첫 실행 중 200으로 변경
두 번째 실행 ❗ 200 그대로 유지