콘텐츠로 이동

[부록] 3-7. 언리얼 빌드 파이프라인 (UHT · UBT)

언리얼에서 C++ 코드를 작성하고 “빌드”를 누르면, 평범한 C++ 프로젝트보다 훨씬 많은 일이 자동으로 일어납니다. 이 과정의 핵심에는 두 가지 도구, UBT(UnrealBuildTool)UHT(UnrealHeaderTool) 가 있습니다. 이 문서에서는 일반 C++ 빌드를 먼저 짚고, 언리얼이 그 위에 무엇을 더 얹는지, 그리고 전체 빌드 순서가 어떻게 되는지를 차례대로 살펴봅니다.

먼저 언리얼이 아닌 보통의 C++ 빌드를 한 문단으로 떠올려 봅시다. 일반적인 C++ 빌드는 크게 세 단계로 진행됩니다. 전처리(Preprocess) 단계에서 #include를 펼치고 매크로를 치환해 하나의 큰 번역 단위(translation unit)를 만들고, 컴파일(Compile) 단계에서 각 .cpp를 기계어 오브젝트 파일(.obj/.o)로 변환하며, 마지막 링크(Link) 단계에서 여러 오브젝트 파일과 라이브러리를 묶어 하나의 실행 파일이나 라이브러리를 만듭니다. 즉 “여러 소스 → 오브젝트 → 하나의 바이너리”가 표준 C++ 빌드의 골격입니다.

언리얼은 위 표준 빌드를 그대로 사용하면서, 그 위에 두 가지 층을 추가합니다.

리플렉션 코드 자동 생성

엔진이 런타임에 타입 정보를 알 수 있도록(에디터 노출, 직렬화, 가비지 컬렉션, 블루프린트 연동 등) 헤더의 매크로를 읽어 별도의 C++ 코드를 컴파일 전에 자동 생성합니다. 이 일을 하는 도구가 UHT입니다.

모듈 시스템

코드를 “모듈” 단위로 나누어 의존성과 빌드 설정을 관리하고, 어떤 모듈을 어떤 순서로 컴파일·링크할지 오케스트레이션합니다. 이 일을 하는 도구가 UBT입니다.

정리하면, 표준 C++ 빌드 위에 (리플렉션 코드 생성)(모듈 단위 빌드 관리) 라는 두 층이 더해진 것이 언리얼 빌드 파이프라인입니다.

언리얼 코드는 파일이나 클래스 하나가 아니라 모듈 단위로 빌드되고 로드됩니다. 모듈은 함께 컴파일되어 하나의 바이너리(주로 DLL)로 묶이는 코드 묶음이며, 다른 모듈에 의존성을 선언해 기능을 빌려 씁니다. 엔진 자체도 수많은 모듈로 쪼개져 있고, 우리가 만드는 게임 코드도 하나 이상의 모듈로 구성됩니다.

이 모듈 구조를 결정하는 것은 몇 개의 설정 파일입니다.

.uproject

프로젝트의 진입점 파일. 어떤 모듈과 플러그인이 포함되는지, 엔진 버전은 무엇인지 등 프로젝트 최상위 정보를 담습니다.

(프로젝트명)Target.cs

빌드 타깃을 정의합니다. 같은 코드를 Editor용으로 빌드할지, 독립 실행 Game으로 빌드할지, Server로 빌드할지 등 “무엇을 위한 빌드인가”를 결정합니다.

(모듈명).Build.cs

개별 모듈의 의존성과 빌드 설정을 정의합니다. 이 모듈이 어떤 다른 모듈에 의존하는지, 어떤 옵션으로 컴파일되는지를 적습니다.

아래는 모듈의 의존성을 선언하는 Build.cs의 모습을 단순화한 예시입니다.

using UnrealBuildTool;
public class MyGame : ModuleRules
{
public MyGame(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
// 이 모듈이 의존하는 다른 모듈들
PublicDependencyModuleNames.AddRange(
new string[] { "Core", "CoreUObject", "Engine", "InputCore" });
}
}

UBT(UnrealBuildTool): 빌드 오케스트레이터

섹션 제목: “UBT(UnrealBuildTool): 빌드 오케스트레이터”

UBT는 C#으로 작성된 빌드 오케스트레이터입니다. 직접 코드를 컴파일하는 컴파일러가 아니라, “무엇을 어떤 설정으로 빌드할지” 계획을 세우고 실제 도구들을 지휘하는 감독 역할을 합니다.

UBT가 하는 일을 정리하면 다음과 같습니다.

  • Target.csBuild.cs를 읽어, 어떤 모듈과 어떤 소스 파일을 어떤 컴파일 옵션·의존성으로 빌드할지 결정합니다.
  • 헤더 처리를 위해 UHT를 호출해 리플렉션 코드를 먼저 생성하게 합니다.
  • 실제 플랫폼 컴파일러(Windows의 MSVC, 또는 Clang 등)링커를 호출해 컴파일·링크를 수행시킵니다.
  • 어떤 파일이 바뀌었는지 추적해, 변경된 부분만 다시 빌드하는 증분 빌드(incremental build) 를 관리합니다.

UHT(UnrealHeaderTool): 리플렉션 코드 생성기

섹션 제목: “UHT(UnrealHeaderTool): 리플렉션 코드 생성기”

UHT는 헤더 파일을 파싱해 리플렉션·직렬화용 C++ 코드를 자동 생성하는 도구입니다. 우리가 헤더에 적는 UCLASS, USTRUCT, UENUM, UPROPERTY, UFUNCTION 같은 매크로가 UHT의 입력입니다. UHT는 이 매크로들을 읽어, 엔진이 런타임에 타입을 이해하는 데 필요한 보조 코드를 만들어 냅니다.

UHT가 만들어 내는 대표 산출물은 두 가지입니다.

파일명.generated.h

각 헤더에 대응해 생성되는 헤더. 클래스 본문에 끼워 넣을 리플렉션 보조 선언이 들어 있으며, GENERATED_BODY()가 이 내용을 펼쳐 줍니다.

파일명.gen.cpp

생성된 리플렉션 정보의 구현부. 타입 등록, 프로퍼티 메타데이터 등 런타임이 사용할 실제 데이터가 여기에 담깁니다.

중요한 점은 UHT가 실제 컴파일 “이전”에 실행된다는 것입니다. 컴파일러가 우리 코드를 읽기 전에 .generated.h.gen.cpp가 먼저 만들어져 있어야, 컴파일러가 원본 코드와 생성 코드를 함께 빌드할 수 있습니다. 참고로 UHT는 UE5에서 C++로 재작성되어, 과거보다 더 빠르고 엔진 빌드 과정에 긴밀하게 통합되었습니다.

GENERATED_BODY()와 generated.h include 위치

섹션 제목: “GENERATED_BODY()와 generated.h include 위치”

클래스 본문 안에 적는 GENERATED_BODY() 매크로는, UHT가 생성한 코드를 그 자리에 끼워 넣는 “삽입 지점” 역할을 합니다. 그리고 그 생성 코드는 .generated.h에 정의되어 있습니다. 따라서 한 가지 규칙이 강제됩니다. .generated.h의 include는 해당 헤더에서 가장 마지막 include여야 합니다.

이유는 생성 코드가 현재 헤더의 타입을 기준으로 만들어지기 때문입니다. generated 헤더 뒤에 다른 include가 더 오면, 매크로 확장 순서가 어긋나 컴파일 오류가 납니다. 아래 형태를 항상 지키면 됩니다.

#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyActor.generated.h" // 항상 "마지막" include
UCLASS()
class MYGAME_API AMyActor : public AActor
{
GENERATED_BODY() // UHT 생성 코드가 여기에 삽입됨
public:
AMyActor();
};

지금까지의 도구들이 실제 빌드에서 어떤 순서로 동작하는지 정리하면 다음과 같습니다.

  1. 빌드 시작: IDE나 에디터에서 빌드를 요청하면 UBT가 실행됩니다.
  2. UBT가 계획 수립: Target.csBuild.cs를 읽어 어떤 타깃·모듈과 어떤 대상 파일을 빌드할지 결정합니다.
  3. UHT가 헤더 파싱: 빌드 대상 헤더의 UCLASS/USTRUCT/UPROPERTY 등 매크로를 분석해 .generated.h.gen.cpp 등 리플렉션 코드를 생성합니다.
  4. 컴파일러가 컴파일: 원본 .cpp와 UHT가 만든 생성 코드를 함께 컴파일해 오브젝트 파일을 만듭니다.
  5. 링커가 바이너리 생성: 오브젝트 파일과 의존 모듈을 묶어 모듈 바이너리(주로 DLL, 또는 실행 파일 exe)를 만듭니다.
  6. 모듈 로드: 에디터나 게임이 이 모듈 바이너리를 로드하면 우리가 작성한 코드가 실제로 동작합니다.

핫 리로드 / 라이브 코딩과의 관계

섹션 제목: “핫 리로드 / 라이브 코딩과의 관계”

위 파이프라인의 최종 산출물은 결국 모듈 바이너리(DLL/exe) 입니다. 코드를 수정한 뒤 빠르게 반영하는 기능들은 모두 이 산출물을 다루는 방식의 차이입니다.

핫 리로드(Hot Reload)

새 모듈 바이너리를 다시 빌드한 뒤, 실행 중인 에디터가 기존 모듈을 새 바이너리로 교체하는 방식입니다.

라이브 코딩(Live Coding)

실행 중인 프로세스의 코드를 다시 빌드한 변경분으로 패치해, 재시작 없이 반영하는 방식입니다.

즉 두 기능 모두 “UBT/UHT/컴파일러/링커가 만든 모듈 산출물을 어떻게 갱신하느냐”의 문제로 볼 수 있습니다. 핫 리로드와 라이브 코딩의 실제 사용법과 차이는 3-6에서 다루었으니, 그 동작의 바탕이 바로 이 빌드 파이프라인이라는 점을 연결해서 이해하면 됩니다.

UBT

C#으로 작성된 빌드 오케스트레이터. 무엇을 어떻게 빌드할지 결정하고 컴파일러·링커를 호출.

UHT

헤더의 리플렉션 매크로를 파싱해 컴파일 전에 생성 코드를 만드는 도구.

Module

함께 빌드·로드되어 하나의 바이너리로 묶이는 코드 단위.

Build.cs

개별 모듈의 의존성과 빌드 설정을 정의하는 C# 스크립트.

Target.cs

Editor/Game/Server 등 빌드 타깃을 정의하는 C# 스크립트.

generated.h

UHT가 생성하는 리플렉션 헤더. 헤더의 마지막 #include 여야 함.

GENERATED_BODY

클래스 본문에 생성 코드를 삽입하는 매크로.