시작하기 전에 - 어떤 코드가 좋은 코드인가
- 변수명 직관적으로 정하기
- 중복 코드 피하기
- 긴 함수 지양하기
리플렉션이란?
실행 중에 프로그램이 자기 자신의 구조를 인지하고 조작할 수 있는 능력. 메타데이터로 블루프린트와 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가 아닌 것을 관리하기 위해 존재.
- 표준 C++: new/delete로 직접 메모리 관리
가비지 컬렉션
동작 원리
언리얼에서 가비지 컬렉션은 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에서 절대 삭제 안 됨
가비지 컬렉션 테스트하기





Super:: 는 수작업으로 작성해줘야하는 것일까?
Rider를 사용하거나 Reshaper 플러그인을 설치하여 Alt + Insert -> Override members를 선택하는 수밖에 없다고 한다.



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






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



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: 출력 내용

❌ UPROPERTY 안 붙이는 경우
1. 단순한 지역 변수 (Local 변수)
{
int a = 10;
}
👉 함수 안에서만 쓰는 변수
→ 엔진이 알 필요 없음
2. 일반 C++ 타입 (엔진 기능 필요 없음)
float Speed;
bool bFlag;
👉 이런 건 그냥 써도 됨
(에디터 노출 / 저장 / 네트워크 필요 없으면)
3. UObject가 아닌 포인터
👉 GC랑 관련 없음 → 필요 없음
4. 임시 계산용 변수
👉 프레임 내 계산용 → 굳이 추적할 필요 없음
5. GC에 영향 주면 안 되는 경우 (중요)
👉 일부러 약한 참조로 쓰는 경우
→ UPROPERTY 없이도 사용 가능 (상황에 따라 붙이기도 함)
6. STL 컨테이너
👉 Unreal은 TArray를 추적함
→ std::vector는 UPROPERTY로 관리 안 됨
⚠️ 반드시 붙여야 하는 경우 (더 중요)
이건 반대로 안 붙이면 큰일 나는 경우 👇
🚨 1. UObject 포인터 (핵심)
👉 GC가 이걸 모름 → 갑자기 삭제됨 (댕글링 포인터)
✔️ 반드시:
TObjectPtr<AActor> MyActor;
🚨 2. 블루프린트에서 써야 할 때
float Speed;
🚨 3. 저장 / 로딩 / 복제 필요
- SaveGame
- Replication
- Editor 노출
위크 포인터 사용 예시: 적 유닛을 저장할 때, 적 유닛이 죽어서 없어질 수도 있어서 위크 포인터로 저장하면 됨
UBT
가장 먼저 실행되는 빌드 도구
전체 프로젝트 구조 파악
플랫폼별 환경 설정
주요 기능들
- 플러그인 및 모듈 검색
- .Target.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
이 함수는 해당 클래스의 CDO(Class Default Object)를 “수정 가능하게” 가져오는 함수
| 첫 실행 전 | 100 |
| 첫 실행 중 | 200으로 변경 |
| 두 번째 실행 | ❗ 200 그대로 유지 |
'게임 개발 > 학습 일지' 카테고리의 다른 글
| 언리얼 C++ - Actor의 라이프 사이클 (0) | 2026.04.11 |
|---|---|
| 언리얼 C++ - Actor 클래스 생성 및 삭제 (0) | 2026.04.11 |
| TA 코스 1주차 정리 (0) | 2026.04.09 |
| 언리얼 C++ - 빌드 프로세스 이해하기 (0) | 2026.04.09 |
| C++ 기초 - STL(Standard Template Library) (0) | 2026.04.09 |