2026-04-10 언리얼 GC / UBT / UHT / CDO / 리플렉션
목차
new는 함수인가?
핵심 답변
new는 함수가 아니라 C++ 연산자(operator) 입니다.
단, 내부적으로 메모리 할당을 위해 operator new라는 함수를 호출합니다.
new 표현식의 실행 단계
1
| Player* p = new Player();
|
위 한 줄이 내부적으로 하는 일:
1
2
3
4
5
6
7
8
| // 1단계: operator new 호출 — 메모리만 할당
void* mem = operator new(sizeof(Player));
// 2단계: 생성자 호출 — 할당된 메모리 위에서 초기화
new(mem) Player(); // placement new로 생성자만 호출
// 3단계: 타입 포인터로 반환
Player* p = static_cast<Player*>(mem);
|
new는 이 3단계를 하나로 묶은 연산자. malloc은 1단계만 하는 함수.
operator new vs new 표현식
| | new 표현식 | operator new |
|---|
| 분류 | 연산자 | 함수 |
| 역할 | 메모리 할당 + 생성자 호출 | 메모리 할당만 |
| 오버로딩 | 불가 | 가능 |
복기
new는 연산자, malloc은 C 라이브러리 함수new 내부에서 operator new(함수)를 호출해 메모리를 확보하고, 이후 생성자를 호출operator new는 오버로딩 가능 — 클래스별 커스텀 메모리 관리 가능
UE_LOG vs AddOnScreenDebugMessage
| 항목 | UE_LOG | AddOnScreenDebugMessage |
|---|
| 출력 위치 | Output Log (콘솔/로그 파일) | 게임 화면 좌상단 |
| 영구성 | 로그 파일에 저장됨 | 일정 시간 후 사라짐 |
| 빌드 | Shipping 빌드에서 제거 가능 | 디버그/개발 빌드 전용 |
| 주 용도 | 디버깅, 로깅, 에러 추적 | 빠른 시각 확인 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // UE_LOG
UE_LOG(LogTemp, Warning, TEXT("Health: %d"), ActorA->Health);
UE_LOG(LogTemp, Error, TEXT("Null pointer!"));
// AddOnScreenDebugMessage
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(
-1, // key: -1이면 매번 새 줄
5.0f, // 표시 시간(초)
FColor::Red,
FString::Printf(TEXT("Health: %d"), Health)
);
}
|
로그 레벨: Display → Warning → Error → Fatal(크래시)
언리얼 GC 작동방식
핵심 답변
언리얼 엔진은 Tracing GC (Mark & Sweep) 방식을 사용합니다.
UObject를 상속한 객체만 GC 대상이며, UPROPERTY()로 선언된 포인터만 GC가 추적합니다.
GC 동작 단계
| 단계 | 설명 |
|---|
| 1. Mark (표시) | Root Set에서 시작해 UPROPERTY로 연결된 UObject를 reference graph로 탐색, 도달 가능한 객체에 “살아있음” 표시 |
| 2. Sweep (정리) | 표시되지 않은 UObject를 메모리에서 해제 |
GC 실행 주기 및 UE5 변경점
- 자동 실행: 기본 30~60초 간격 (가용 메모리에 따라 더 자주 실행될 수 있음)
- 기존 GC 한계: 모든 UObject를 한 번에 순회 → 오브젝트가 많으면 프레임 히치 발생
- UE5 Incremental GC: 여러 프레임에 걸쳐 점진적으로 GC 수행 → 히치 감소
ForceGarbageCollection() 직접 호출 가능 (프레임 히치 주의)
UPROPERTY() 역할
1
2
3
4
5
6
7
8
9
10
| UCLASS()
class AMyActor : public AActor
{
GENERATED_BODY()
public:
UPROPERTY() // GC가 이 포인터를 추적 → 수거 안 됨
UMyComponent* SafeComp;
UMyComponent* DangerousComp; // UPROPERTY 없음 → GC가 수거할 수 있음!
};
|
UPROPERTY() 없는 포인터는 GC가 참조를 모름 → 객체 수거 후 댕글링 포인터 발생
복기
- GC 대상:
UObject 상속 클래스만 - UPROPERTY(): GC 추적의 핵심 — 없으면 수거됨
- IsValid(): GC 수거 여부 런타임 확인
- AddToRoot() / RemoveFromRoot(): 강제로 GC 수거 막기/풀기
핵심 역할
UBT는 빌드 프로세스의 첫 번째 관문입니다.
가장 먼저 실행되어 전체 프로젝트 구조를 파악하고 플랫폼별 환경을 설정합니다.
빌드 흐름
1
| UBT 실행 → 환경 설정 → UHT 실행 → 컴파일 → 링크
|
주요 기능
- 플러그인 및 모듈 검색 — Plugins, Modules 디렉터리 탐색
.Target.cs 파일 분석 — 빌드 대상(Editor/Game/Server 등) 결정.Build.cs 파일 처리 — 모듈별 의존성 및 컴파일 설정- 플랫폼 호환성 확인 — Win64, Mac, Linux 등
- 개발 환경 자동 구성 — VS/Xcode 프로젝트 파일 생성
Rules Assembly 동작 방식
.Build.cs 및 .Target.cs는 C# 파일이며, UBT가 이를 Rules Assembly(공유 라이브러리)로 컴파일한 뒤 실행- 빌드 규칙이 단순 텍스트가 아닌 실행 가능한 C# 코드 → 조건분기, 루프 등 로직 작성 가능
1
2
3
4
5
6
7
8
9
10
11
| public class MyGame : ModuleRules
{
public MyGame(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[]
{
"Core", "CoreUObject", "Engine", "InputCore"
});
}
}
|
복기
- UBT는 빌드 도구, UHT는 헤더 분석 도구 — 역할이 다름
.Build.cs/.Target.cs는 C# 코드 → Rules Assembly로 컴파일되어 실행.Build.cs 없으면 모듈이 인식 안 됨
핵심 역할
UHT는 컴파일 전 마법사입니다.
.h 파일에서 UCLASS, UPROPERTY, UFUNCTION, USTRUCT, UENUM 매크로를 읽어 메타데이터(리플렉션 정보) 를 생성합니다.
UHT가 생성하는 파일
| 파일 | 설명 |
|---|
MyObject.generated.h | helper 함수 및 thunk 함수 포함 (헤더별 1개) |
MyObject.gen.cpp | 리플렉션 데이터 실체 구현 (헤더별 1개) |
[Module].generated.inl | 모듈 단위 리플렉션 데이터 통합 (모듈별 1개) |
생성된 리플렉션 데이터는 바이너리와 함께 컴파일되므로 구식화(stale) 되는 일이 없음
1
2
3
4
5
6
7
8
9
10
11
| UCLASS()
class AMyActor : public AActor
{
GENERATED_BODY() // UHT가 generated.h를 삽입하는 자리
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 Health = 100;
UFUNCTION(BlueprintCallable)
void TakeDamage(int32 Amount);
};
|
복기
- UHT는 컴파일 전에 실행, generated.h를 만들어 컴파일러에 제공
GENERATED_BODY() 없으면 컴파일 에러- UPROPERTY/UFUNCTION이 없으면 에디터·블루프린트에서 안 보임
리플렉션 시스템
리플렉션이란?
런타임에 클래스/변수/함수의 타입 정보에 접근하는 기능입니다.
C++ 표준에는 리플렉션이 없기 때문에, UE는 UHT + 매크로로 자체 구현했습니다.
빌드 흐름
1
2
3
4
5
| UBT 실행
└→ UHT 실행: UCLASS/UPROPERTY/UFUNCTION 매크로 분석
└→ MyClass.generated.h 생성 (리플렉션 데이터 포함)
└→ 컴파일러가 generated.h 포함해 컴파일
└→ 런타임: 리플렉션 시스템 활성화
|
리플렉션 타입 계층 (UE 내부 표현)
| C++ 원본 | UE 타입 | 역할 |
|---|
| class (UObject 상속) | UClass | 클래스 메타데이터 |
| struct | UScriptStruct | 구조체 메타데이터 |
| function | UFunction | 함수 메타데이터 |
| enum | UEnum | 열거형 메타데이터 |
| member variable | FProperty | 멤버 변수 메타데이터 (UE5에서 UProperty → FProperty 리네임) |
UPROPERTY() 하나가 담당하는 것
1
2
3
4
5
6
7
8
| UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 Health = 100;
// ↑ 이 한 줄이:
// 1. 에디터 디테일 패널 노출
// 2. 블루프린트에서 읽기/쓰기 가능
// 3. GC 추적 대상 등록
// 4. 직렬화(저장/로드) 자동 처리
// 5. CDO → 인스턴스 복사 시 값 전달
|
C++ RTTI vs UE 리플렉션
| | C++ RTTI | UE 리플렉션 |
|---|
| 에디터 연동 | 불가 | 가능 |
| GC 연동 | 불가 | 가능 |
| UE에서 사용 | 금지 (비활성화) | 권장 |
CDO — Class Default Object
CDO란?
UObject 클래스마다 하나씩 존재하는 기본값 보관소입니다.
붕어빵 틀(CDO) → 붕어빵들(Instance Objects)
생성 시점
1
2
| UBT → UHT → 컴파일 → 엔진 시작 시 CDO 생성 (생성자 딱 한 번 호출)
Module Loading → CDO Created → Instances Copy from CDO
|
CDO 접근 방법
1
2
3
4
5
6
7
8
| // 공식 방법 — StaticClass()를 통한 접근
ATestMyActor* CDO = Cast<ATestMyActor>(ATestMyActor::StaticClass()->GetDefaultObject());
// 헬퍼 함수 — 수정 가능
ATestMyActor* MutableCDO = GetMutableDefault<ATestMyActor>();
// 헬퍼 함수 — 읽기 전용
const ATestMyActor* ReadOnlyCDO = GetDefault<ATestMyActor>();
|
C++ 클래스 vs 블루프린트 클래스의 CDO 차이
| | C++ 클래스 | 블루프린트 클래스 |
|---|
| CDO 기본값 출처 | C++ 생성자 | .uasset 파일 |
| 에디터 수정 | 컴파일 시점에 고정 | 블루프린트 에디터에서 저장 |
CDO 최적화 원리
| 원리 | 설명 |
|---|
| 메모리 절약 | 공통 기본값을 CDO 하나에만 저장 |
| 빠른 초기화 | 생성자 호출 없이 메모리 복사로 즉시 초기화 |
| 델타 직렬화 | CDO와 차이점만 기록·전송 → 파일 용량·네트워크 효율 극대화 |
CDO 코드 실습 분석
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| void AMyGameModeBase::BeginPlay()
{
Super::BeginPlay();
ATestMyActor* ActorA = GetWorld()->SpawnActor<ATestMyActor>(); // CDO 기본값 100 복사
UE_LOG(LogTemp, Warning, TEXT("ActorA Init Health : %d"), ActorA->Health); // 100
ActorA->Health = 50; // 인스턴스만 변경, CDO 무관
UE_LOG(LogTemp, Warning, TEXT("ActorA Modi : %d"), ActorA->Health); // 50
ATestMyActor* MyActorCDO = GetMutableDefault<ATestMyActor>();
UE_LOG(LogTemp, Warning, TEXT("CDO Init Health : %d"), MyActorCDO->Health); // 100
MyActorCDO->Health = 200; // CDO 수정
UE_LOG(LogTemp, Warning, TEXT("CDO Modfi Health : %d"), MyActorCDO->Health); // 200
ATestMyActor* ActorB = GetWorld()->SpawnActor<ATestMyActor>(); // CDO가 200이므로 200 복사
UE_LOG(LogTemp, Warning, TEXT("ActorB Init Health : %d"), ActorB->Health); // 200
UE_LOG(LogTemp, Warning, TEXT("ActorA Current HP : %d"), ActorA->Health); // 50 (변화 없음)
}
|
| 로그 | 출력값 | 이유 |
|---|
| ActorA Init Health | 100 | CDO 기본값(100)에서 복사 |
| ActorA Modi | 50 | 인스턴스 값만 변경 |
| CDO Init Health | 100 | CDO 원본값 |
| CDO Modfi Health | 200 | CDO 자체를 200으로 수정 |
| ActorB Init Health | 200 | CDO가 200인 후 생성 → 200 복사 |
| ActorA Current HP | 50 | 이미 생성된 인스턴스는 CDO 변경 영향 없음 |
주의: 같은 PIE 세션 내에서 CDO 수정은 이후 SpawnActor에 영향을 줌.
PIE 재실행 시 SpawnActor는 생성자 기본값을 사용하지만, GetMutableDefault가 반환하는 CDO 객체에는 이전 수정값이 남아있을 수 있음 → 상태 불일치 발생 가능
언리얼 엔진 C++ 코드 컨벤션
명명 규칙
| 접두사 | 의미 | 예시 |
|---|
| T | 템플릿 클래스 | TArray, TMap |
| U | UObject 상속 | UStaticMeshComponent |
| A | AActor 상속 | ACharacter, AGameMode |
| S | SWidget 상속 (Slate) | SButton |
| I | 추상 인터페이스 | IInterface |
| E | 열거형 | ECollisionChannel |
| F | 그 외 대부분 | FVector, FString |
| b | bool 변수 | bIsDead, bHasFadedIn |
이식성 타입
| C++ 기본 타입 | UE 타입 | 크기 보장 |
|---|
| int | int32 | 항상 32비트 |
| unsigned int | uint32 | 항상 32비트 |
| unsigned char | uint8 | 항상 8비트 |
| long long | int64 | 항상 64비트 |
| std::string | FString | UE 문자열 |
언어 기능 제한
| 항목 | 대신 사용 |
|---|
| RTTI (dynamic_cast, typeid) | UE 자체 리플렉션 시스템 |
| C++ 예외 (try/catch) | check() / ensure() |
| std::string | FString |
| new (UObject) | NewObject<T>() |
| goto | 사용 금지 |
1
2
3
4
5
6
7
| // UObject 생성
UMyComponent* Comp = NewObject<UMyComponent>(this); // 올바름
// UMyComponent* Comp = new UMyComponent(); // 금지
// 오류 처리
check(Pointer != nullptr); // 치명적, 크래시
ensure(Value > 0); // 비치명적, 로그 후 계속
|