언리얼 엔진은 거대한 단일 코드 덩어리가 아니라 모듈 단위로 나뉘어 빌드되고 로드되는 구조다.
모듈은 Build.cs를 통해 의존성을 정의하고, UBT는 이 정보를 기반으로 빌드 순서를 결정한다.
UHT는 UCLASS, UPROPERTY, UFUNCTION 같은 언리얼 매크로를 분석해 리플렉션 코드를 생성한다.
모듈 시스템
| 구분 | 역할 |
|---|---|
| Runtime | 게임 실행에 필요한 코드 |
| Editor | 에디터 전용 코드 |
| Developer | 개발 도구, 프로파일링, 테스트 코드 |
| ThirdParty | 외부 라이브러리 |
| Programs | UBT, UHT, UnrealPak 같은 도구 프로그램 |
모듈은 언리얼 코드의 기본 빌딩 블록이다.
| 개념 | 설명 |
|---|---|
| Module | 코드의 기본 단위. Build.cs 하나를 가진다 |
| Plugin | 하나 이상의 모듈을 담는 컨테이너 |
| Project | 게임 자체. .uproject로 정의된다 |
모듈은 Public과 Private 폴더로 나누어 접근 범위를 관리한다.
| 폴더 | 의미 |
|---|---|
| Public | 외부 모듈에서 include 가능 |
| Private | 해당 모듈 내부에서만 사용 |
Runtime / Editor Module
모듈은 타입에 따라 패키징 포함 여부가 달라진다.
| 타입 | 용도 | 패키징 포함 |
|---|---|---|
| Runtime | 실제 게임 실행 코드 | 포함 |
| Editor | 에디터 확장 코드 | 제외 |
| Developer | 개발 빌드용 도구 코드 | Shipping 제외 |
중요한 규칙은 다음과 같다.
| 관계 | 가능 여부 |
|---|---|
| Editor → Runtime 참조 | 가능 |
| Runtime → Editor 참조 | 불가능 |
Runtime 모듈에서 에디터 전용 코드를 써야 한다면 #if WITH_EDITOR로 감싸야 한다.
#if WITH_EDITOR
// 에디터 전용 코드
#endif
Loading Phase
LoadingPhase는 모듈이 언제 로드될지 결정한다.
| Phase | 용도 |
|---|---|
| EarliestPossible | 가장 빠른 시점 |
| PostConfigInit | 설정 시스템 초기화 직후 |
| PreDefault | 기본 로드보다 먼저 |
| Default | 일반적인 게임플레이 모듈 |
| PostEngineInit | 엔진 초기화 후 |
| None | 자동 로드하지 않음 |
일반적인 게임 모듈은 대부분 Default를 사용한다.
MODULE_API 매크로
다른 모듈에서 접근해야 하는 클래스에는 MODULE_API 매크로가 필요하다.
UCLASS()
class MYGAME_API AMyCharacter : public ACharacter
{
GENERATED_BODY()
};
| 빌드 방식 | 역할 |
|---|---|
| Modular | dllexport / dllimport 역할 |
| Monolithic | 빈 매크로로 처리 |
MYGAME_API가 없으면 다른 모듈에서 클래스를 참조할 때 링크 에러가 발생할 수 있다.
Plugin 구조
플러그인은 .uplugin 파일로 정의되고, 내부에 여러 모듈을 포함할 수 있다.
{
"Modules": [
{
"Name": "MyPlugin",
"Type": "Runtime",
"LoadingPhase": "Default"
},
{
"Name": "MyPluginEditor",
"Type": "Editor",
"LoadingPhase": "Default"
}
]
}
플러그인 구조의 핵심 규칙은 다음과 같다.
| 규칙 | 설명 |
|---|---|
| 모듈당 Build.cs 하나 | 모듈명.Build.cs 필요 |
| 최소 cpp 필요 | 모듈로 인식되려면 최소 하나의 cpp 필요 |
| IMPLEMENT_MODULE 필요 | 모듈 등록 매크로 필요 |
| Runtime / Editor 분리 | 게임 코드와 에디터 코드를 분리해야 함 |
IMPLEMENT_MODULE(FMyPluginModule, MyPlugin)
Build.cs
모듈의 빌드 설정을 정의하는 C# 파일
public class MyGame : ModuleRules
{
public MyGame(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
bEnforceIWYU = true;
PublicDependencyModuleNames.AddRange(new string[]
{
"Core",
"CoreUObject",
"Engine"
});
PrivateDependencyModuleNames.AddRange(new string[]
{
"Slate",
"SlateCore"
});
}
}
주요 설정
| 설정 | 의미 |
|---|---|
PCHUsage |
Precompiled Header 사용 방식 |
bEnforceIWYU |
필요한 헤더만 include하도록 강제 |
PublicDependencyModuleNames |
Public 헤더에서 필요한 모듈 |
PrivateDependencyModuleNames |
cpp 내부에서만 필요한 모듈 |
Public Dependency와 Private Dependency
PublicDependencyModuleNames와 PrivateDependencyModuleNames의 차이는 의존성 전파 여부다.
| 구분 | 의미 | 전파 여부 |
|---|---|---|
| Public Dependency | Public 헤더에서 사용하는 모듈 | 전파됨 |
| Private Dependency | cpp 내부에서만 사용하는 모듈 | 전파되지 않음 |
Public 헤더에서 UGameplayAbility를 사용한다면 GameplayAbilities는 Public Dependency에 있어야 한다.
// MyAbility.h
#include "Abilities/GameplayAbility.h"
UCLASS()
class UMyAbility : public UGameplayAbility
{
GENERATED_BODY()
};
PublicDependencyModuleNames.Add("GameplayAbilities");
반대로 cpp에서만 UMG를 사용한다면 Private Dependency로 충분하다.
PrivateDependencyModuleNames.Add("UMG");
UBT 빌드 흐름
UBT는 빌드 시작 시 모듈 정보를 읽고 의존성 그래프를 만든다.
flowchart LR
A[uproject / uplugin 스캔] --> B[Build.cs 파싱]
B --> C[모듈 의존성 그래프 생성]
C --> D[순환 의존성 검사]
D --> E[빌드 순서 결정]
E --> F[UHT 실행]
F --> G[C++ 컴파일]
G --> H[링킹]
UBT는 모듈 간 의존성을 그래프로 만들고, 위상 정렬을 통해 빌드 순서를 결정한다.
| 순서 | 모듈 |
|---|---|
| 1 | Core |
| 2 | CoreUObject |
| 3 | Engine |
| 4 | GameplayAbilities |
| 5 | MyGame |
순환 의존성이 있으면 빌드가 실패한다.
| 문제 | 해결 |
|---|---|
| ModuleA → ModuleB → ModuleA | 공통 모듈 분리 |
| 서로 직접 참조 | 인터페이스 모듈 도입 |
| 구조가 꼬임 | 의존성 방향 재설계 |
UHT와 리플렉션
언리얼 리플렉션 코드를 생성하는 도구
UHT가 처리하는 대상
| 매크로 | 의미 |
|---|---|
UCLASS |
UObject 기반 클래스 |
USTRUCT |
리플렉션 가능한 구조체 |
UENUM |
리플렉션 가능한 enum |
UPROPERTY |
리플렉션 프로퍼티 |
UFUNCTION |
리플렉션 함수 |
UINTERFACE |
언리얼 인터페이스 |
UHT가 처리하려면 헤더에 .generated.h가 있어야 하며, 이 include는 반드시 마지막에 위치해야 한다.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyActor.generated.h"
UCLASS()
class MYGAME_API AMyActor : public AActor
{
GENERATED_BODY()
};
generated.h와 gen.cpp
UHT는 Intermediate 폴더에 리플렉션 코드를 생성한다.
| 파일 | 역할 |
|---|---|
.generated.h |
클래스 선언 보조 코드 생성 |
.gen.cpp |
리플렉션 정보의 실제 구현 생성 |
generated.h는 다음 기능을 제공한다.
| 기능 | 설명 |
|---|---|
StaticClass() |
클래스의 UClass 반환 |
GetClass() |
인스턴스의 클래스 정보 조회 |
| 네이티브 함수 등록 | UFUNCTION을 리플렉션 시스템에 등록 |
| 직렬화 지원 | 저장, 로드, 복제 지원 |
| 메타데이터 연결 | 클래스, 프로퍼티, 함수 정보 등록 |
.gen.cpp는 실제로 UClass, FProperty, UFunction 정보를 등록한다.
이 덕분에 다음 기능이 가능해진다.
| 기능 | 설명 |
|---|---|
| 에디터 디테일 패널 | UPROPERTY 정보를 읽어 UI 생성 |
| 블루프린트 노드 | UFUNCTION 정보를 읽어 노드 생성 |
| GC | UPROPERTY로 표시된 UObject 참조 추적 |
| 네트워크 복제 | 복제 대상 프로퍼티 확인 |
| 동적 함수 호출 | ProcessEvent()로 함수 호출 |
UCLASS가 인식되지 않습니다, generated.h를 찾을 수 없습니다 같은 에러는 보통 아래 원인 중 하나다.
| 원인 | 해결 |
|---|---|
| Build.cs 의존성 누락 | 필요한 모듈 추가 |
.generated.h include 누락 |
헤더 마지막에 include |
GENERATED_BODY() 누락 |
클래스 본문에 추가 |
| include 순서 문제 | .generated.h를 마지막에 배치 |
| 플러그인 비활성화 | .uproject에서 플러그인 활성화 |
정적 리플렉션
빌드 시점에 리플렉션 코드를 생성하는 정적 리플렉션 방식
정적 리플렉션의 장점은 다음과 같다.
| 장점 | 설명 |
|---|---|
| 빠른 런타임 조회 | 메타데이터가 C++ 코드로 생성됨 |
| 컴파일 타임 검증 | 잘못된 리플렉션 사용은 빌드 에러로 잡힘 |
| 에디터 통합 | 디테일 패널, 블루프린트 노드 생성 가능 |
| GC 통합 | UObject 참조 추적 가능 |
| 네트워크 복제 통합 | Replication 시스템과 연결 가능 |
GC와 UPROPERTY
언리얼 GC는 UPROPERTY로 표시된 UObject* 참조를 추적한다.
UCLASS()
class AMyActor : public AActor
{
GENERATED_BODY()
public:
UPROPERTY()
TObjectPtr<UMaterial> CachedMaterial;
UMaterial* RawMaterial;
};
| 변수 | GC 추적 |
|---|---|
CachedMaterial |
추적됨 |
RawMaterial |
추적 안 됨 |
따라서 UObject 멤버는 기본적으로 UPROPERTY()를 붙이는 것이 안전하다.
UPROPERTY()
TObjectPtr<UStaticMeshComponent> MeshComponent;
임시 값이지만 GC 추적은 필요하고 저장은 원하지 않는다면 Transient를 사용할 수 있다.
UPROPERTY(Transient)
TObjectPtr<UMaterial> TempMaterial;
UPROPERTY Specifier
UPROPERTY 지정자는 크게 에디터 노출, 블루프린트 노출, 복제, 직렬화, 메모리 관리로 나눌 수 있다.
| 분류 | 주요 지정자 |
|---|---|
| 에디터 노출 | EditAnywhere, EditDefaultsOnly, VisibleAnywhere |
| 블루프린트 노출 | BlueprintReadOnly, BlueprintReadWrite |
| 복제 | Replicated, ReplicatedUsing |
| 직렬화 | Transient, SaveGame, SkipSerialization |
| 메모리 | Instanced, Export, NoClear |
UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated)
float Health;
EditAnywhere, BlueprintReadWrite, Replicated 같은 정보는 런타임 플래그로 저장된다.
meta=(DisplayName, ClampMin, ToolTip) 같은 메타데이터는 에디터 전용이며, Shipping 빌드에서는 제거될 수 있다.
UPROPERTY(EditAnywhere, meta=(ClampMin="0", ClampMax="100"))
float Health;
런타임 메타데이터 접근
모든 UCLASS는 런타임에 UClass를 통해 클래스 정보를 조회할 수 있다.
UClass* MyClass = AMyCharacter::StaticClass();
FString ClassName = MyClass->GetName();
UClass* SuperClass = MyClass->GetSuperClass();
bool bIsCharacter = MyClass->IsChildOf(ACharacter::StaticClass());
프로퍼티도 순회할 수 있다.
for (TFieldIterator<FProperty> It(MyClass); It; ++It)
{
FProperty* Prop = *It;
UE_LOG(LogTemp, Log, TEXT("Property: %s"), *Prop->GetName());
}
함수는 UFunction으로 찾고, ProcessEvent()로 동적 호출할 수 있다.
UFunction* Func = MyClass->FindFunctionByName(TEXT("TakeDamage"));
if (Func)
{
struct FParams
{
float Amount;
};
FParams Params{25.0f};
MyCharacter->ProcessEvent(Func, &Params);
}
이 구조가 블루프린트 호출, 콘솔 명령어, RPC, 에디터 기능의 기반이 된다.
RepNotify
ReplicatedUsing은 특정 프로퍼티가 클라이언트에 복제되었을 때 콜백 함수를 호출한다.
UPROPERTY(ReplicatedUsing=OnRep_Health)
float Health;
UFUNCTION()
void OnRep_Health();
흐름은 다음과 같다.
flowchart LR
A[서버에서 값 변경] --> B[NetDriver가 변경 감지]
B --> C[프로퍼티 직렬화]
C --> D[클라이언트 전송]
D --> E[클라이언트에서 값 적용]
E --> F[OnRep 함수 호출]
OnRep 함수의 시그니처는 정확해야 한다.
| 형태 | 가능 여부 |
|---|---|
void OnRep_Health() |
가능 |
void OnRep_Health(float OldHealth) |
가능 |
void OnRep_Health(int32 WrongType) |
불가 |
void OnRep_Health() const |
불가 |
블루프린트 노출 원리
블루프린트 노드는 UFUNCTION의 리플렉션 정보를 읽어서 생성된다.
UFUNCTION(BlueprintCallable, Category="Combat")
float ApplyDamage(float BaseDamage, AActor* DamageCauser);
블루프린트 시스템은 다음 정보를 읽는다.
| 정보 | 사용처 |
|---|---|
| 함수 이름 | 노드 이름 |
| Category | 노드 분류 |
| 파라미터 타입 | 입력 핀 생성 |
| 반환 타입 | 출력 핀 생성 |
| Meta 태그 | 표시 이름, 툴팁, 검색 키워드 |
Meta 태그로 노드 표시 방식을 조정할 수 있다.
UFUNCTION(BlueprintCallable, Category="Combat",
meta=(DisplayName="데미지 적용", Keywords="hurt attack damage"))
void ApplyDamage(float Amount);
런타임 로딩 흐름
언리얼 실행 시 모듈은 대략 다음 순서로 로드된다.
flowchart LR
A[Core] --> B[CoreUObject]
B --> C[Engine]
C --> D[Plugin Modules]
D --> E[Project Module]
E --> F[Editor Modules]
모듈은 IModuleInterface를 구현하고, StartupModule()과 ShutdownModule()에서 초기화와 정리를 수행할 수 있다.
class FMyGameModule : public IModuleInterface
{
public:
virtual void StartupModule() override;
virtual void ShutdownModule() override;
};
게임 모듈은 보통 다음 매크로로 등록한다.
IMPLEMENT_PRIMARY_GAME_MODULE(FMyGameModule, MyGame, "MyGame");
일반 모듈은 다음을 사용한다.
IMPLEMENT_MODULE(FMyPluginModule, MyPlugin)
StaticClass와 UClass 생성
StaticClass()는 해당 클래스의 UClass를 반환한다.
UClass* Class = AMyCharacter::StaticClass();
언리얼은 모든 UClass를 시작 시점에 전부 만드는 것이 아니라, 필요할 때 생성하는 Lazy Initialization 방식을 사용한다.
| 장점 | 설명 |
|---|---|
| 시작 시간 절약 | 사용하지 않는 클래스는 늦게 초기화 |
| 메모리 절약 | 필요한 클래스만 초기화 |
.gen.cpp의 Z_Construct_UClass_XXX() 함수가 실제 UClass를 만든다.
이 과정에서 부모 클래스, 프로퍼티, 함수, 메타데이터, 인터페이스 정보가 연결된다.
CDO
CDO는 Class Default Object의 약자로, 각 UCLASS마다 하나씩 존재하는 기본 템플릿 객체다.
| 특징 | 설명 |
|---|---|
| 클래스당 하나 | 모든 UCLASS는 CDO를 가진다 |
| 기본값 저장 | 프로퍼티 기본값을 보관 |
| 복사 원본 | 새 인스턴스 생성 시 기준이 됨 |
| Reset 기준 | 에디터의 Reset to Default 기준 |
| BP 기본값 | 블루프린트 기본값 편집은 CDO 편집과 연결됨 |
CDO는 다음처럼 접근할 수 있다.
AMyCharacter* CDO = GetMutableDefault<AMyCharacter>();
const AMyCharacter* ConstCDO = GetDefault<AMyCharacter>();
생성자는 CDO를 초기화할 때 호출된다.
따라서 생성자에서는 GetWorld()나 GetOwner()를 사용하는 것이 안전하지 않다.
AMyCharacter::AMyCharacter()
{
Health = 100.0f;
MeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
}
월드가 필요한 초기화는 BeginPlay()에서 처리하는 것이 맞다.
Blueprint 클래스 로딩
블루프린트 클래스의 실제 타입은 UBlueprintGeneratedClass다.
블루프린트 에셋과 실제 생성 클래스는 다르다.
| 구분 | 의미 |
|---|---|
UBlueprint |
에디터에서 편집하는 에셋 |
UBlueprintGeneratedClass |
실제 런타임에서 사용하는 클래스 |
블루프린트 클래스를 직접 로드할 때는 _C 접미사가 붙은 경로를 사용한다.
UClass* BPClass = LoadClass<AActor>(
nullptr,
TEXT("/Game/Blueprints/BP_MyCharacter.BP_MyCharacter_C")
);
_C가 붙은 경로는 GeneratedClass를 의미한다.
최종 요약
언리얼의 모듈, 빌드, 리플렉션 구조는 다음 흐름으로 연결된다.
flowchart LR
A[Module] --> B[Build.cs]
B --> C[UBT]
C --> D[UHT]
D --> E[generated.h / gen.cpp]
E --> F[Reflection]
F --> G[Editor / Blueprint / GC / Replication]
핵심은 다음과 같다.
| 개념 | 핵심 |
|---|---|
| Module | 언리얼 코드의 기본 단위 |
| Build.cs | 모듈의 빌드 설정과 의존성 정의 |
| UBT | 모듈 그래프를 만들고 빌드 순서 결정 |
| UHT | 언리얼 매크로를 파싱해 리플렉션 코드 생성 |
| generated.h / gen.cpp | UClass, FProperty, UFunction 등록 코드 |
| Reflection | 에디터, 블루프린트, GC, 네트워크 복제의 기반 |
| CDO | 클래스 기본값을 가진 템플릿 객체 |
| BlueprintGeneratedClass | 블루프린트의 실제 런타임 클래스 |
언리얼 C++은 일반 C++ 코드 위에 UBT, UHT, 리플렉션, CDO, 모듈 시스템이 얹혀 있는 구조다.
Build.cs 의존성, generated.h, UPROPERTY, UFUNCTION, MODULE_API, CDO 생성 시점까지 함께 이해해야 안정적으로 언리얼 프로젝트를 설계할 수 있다.
'Unreal Engine' 카테고리의 다른 글
| Unreal Engine Tick 최적화 (0) | 2026.07.06 |
|---|---|
| UE 애니메이션 리타게팅 (0) | 2026.07.03 |
| UE MVVM Plugin (0) | 2026.07.01 |
| Asset Manager + PrimaryAsset (0) | 2026.06.30 |
| 언리얼 레퍼런스 (0) | 2026.06.29 |