모듈 구조 이해
Build.cs, 의존성 관리, Public/Private 분리의 핵심 개념
언리얼 엔진 모듈 시스템
UE5 프로젝트의 기본 구성 단위인 모듈의 개념과 구조
모듈이란?
언리얼 엔진에서 모듈(Module)은 관련된 C++ 코드를 논리적 단위로 그룹화한 것입니다. 각 모듈은 독립적으로 컴파일되며, 다른 모듈과의 의존성을 명시적으로 선언합니다.
- 독립적인 컴파일 단위
- 명시적 의존성 관리
- 캡슐화된 API (Public/Private 분리)
- 재사용 가능한 코드 패키지
모듈 디렉토리 구조
MyModule/
├── MyModule.Build.cs // 빌드 설정 파일
├── Public/ // 외부 모듈에 노출할 헤더
│ ├── MyModule.h // 모듈 헤더
│ └── MyClass.h // 공개 클래스
└── Private/ // 내부 전용 파일
├── MyModule.cpp // 모듈 구현
├── MyClass.cpp // 클래스 구현
└── InternalHelper.h // 내부 헬퍼 (비공개)
- Public/ - 다른 모듈에서 #include 가능한 헤더만 배치
- Private/ - 모듈 내부에서만 사용하는 모든 파일 (.h, .cpp)
- 이 분리로 불필요한 의존성과 컴파일 시간을 줄일 수 있음
Build.cs 파일 작성
모듈의 빌드 설정과 의존성을 정의하는 핵심 파일
기본 Build.cs 구조
using UnrealBuildTool;
public class MyGameModule : ModuleRules
{
public MyGameModule(ReadOnlyTargetRules Target) : base(Target)
{
// PCH 설정
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
// Public 의존성 - 이 모듈을 사용하는 다른 모듈도 접근 가능
PublicDependencyModuleNames.AddRange(new string[]
{
"Core",
"CoreUObject",
"Engine",
"InputCore"
});
// Private 의존성 - 이 모듈 내부에서만 사용
PrivateDependencyModuleNames.AddRange(new string[]
{
"Slate",
"SlateCore",
"UMG"
});
}
}
의존성 타입 비교
| 의존성 타입 | 설명 | 사용 시점 |
|---|---|---|
PublicDependencyModuleNames |
Public 헤더에서 사용하는 모듈 | 공개 API에 타입이 노출될 때 |
PrivateDependencyModuleNames |
Private 코드에서만 사용하는 모듈 | 구현 세부사항에서만 필요할 때 |
DynamicallyLoadedModuleNames |
런타임에 동적 로딩하는 모듈 | 선택적 기능, 플러그인 |
고급 Build.cs 설정
using UnrealBuildTool;
using System.IO;
public class AdvancedModule : ModuleRules
{
public AdvancedModule(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
// Include What You Use (IWYU) 강제
bEnforceIWYU = true;
// C++ 버전 설정
CppStandard = CppStandardVersion.Cpp20;
// Public Include 경로 추가
PublicIncludePaths.AddRange(new string[]
{
Path.Combine(ModuleDirectory, "Public"),
Path.Combine(ModuleDirectory, "Public/Interfaces")
});
// Private Include 경로 추가
PrivateIncludePaths.AddRange(new string[]
{
Path.Combine(ModuleDirectory, "Private"),
Path.Combine(ModuleDirectory, "Private/Core")
});
// 플랫폼별 설정
if (Target.Platform == UnrealTargetPlatform.Win64)
{
// Windows 전용 설정
PublicDefinitions.Add("PLATFORM_WINDOWS=1");
}
// 에디터 전용 코드 분리
if (Target.bBuildEditor)
{
PrivateDependencyModuleNames.Add("UnrealEd");
}
}
}
순환 의존성(Circular Dependency)은 컴파일 오류를 유발합니다. 모듈 A가 B를 의존하고, B가 A를 의존하면 안 됩니다. 이를 해결하려면:
- 공통 인터페이스를 별도 모듈로 분리
- Forward Declaration 활용
- 모듈 구조 재설계
모듈 구현 코드
IModuleInterface를 상속받아 모듈의 시작/종료를 관리
모듈 헤더 파일
#pragma once
#include "Modules/ModuleManager.h"
// 모듈 API 매크로 정의
#if !defined(MYGAMEMODULE_API)
#define MYGAMEMODULE_API DLLEXPORT
#endif
class FMyGameModule : public IModuleInterface
{
public:
/** IModuleInterface 구현 */
virtual void StartupModule() override;
virtual void ShutdownModule() override;
/** 핫 리로드 지원 여부 */
virtual bool SupportsDynamicReloading() override
{
return true;
}
/** 싱글톤 접근자 */
static FMyGameModule& Get()
{
return FModuleManager::LoadModuleChecked<FMyGameModule>("MyGameModule");
}
/** 모듈 로드 상태 확인 */
static bool IsAvailable()
{
return FModuleManager::Get().IsModuleLoaded("MyGameModule");
}
/** 커스텀 기능 */
void RegisterGameSystems();
void UnregisterGameSystems();
private:
// 모듈 내부 상태
bool bSystemsRegistered = false;
};
모듈 구현 파일
#include "MyGameModule.h"
#include "Modules/ModuleManager.h"
#define LOCTEXT_NAMESPACE "FMyGameModule"
void FMyGameModule::StartupModule()
{
// 모듈이 로드될 때 실행
UE_LOG(LogTemp, Log, TEXT("MyGameModule: StartupModule"));
// 게임 시스템 초기화
RegisterGameSystems();
}
void FMyGameModule::ShutdownModule()
{
// 모듈이 언로드될 때 실행
UE_LOG(LogTemp, Log, TEXT("MyGameModule: ShutdownModule"));
// 정리 작업
UnregisterGameSystems();
}
void FMyGameModule::RegisterGameSystems()
{
if (!bSystemsRegistered)
{
// 커스텀 시스템 등록
// 예: 에셋 타입 등록, 커스텀 콘솔 명령 등
bSystemsRegistered = true;
UE_LOG(LogTemp, Log, TEXT("Game systems registered"));
}
}
void FMyGameModule::UnregisterGameSystems()
{
if (bSystemsRegistered)
{
// 등록한 시스템 해제
bSystemsRegistered = false;
UE_LOG(LogTemp, Log, TEXT("Game systems unregistered"));
}
}
#undef LOCTEXT_NAMESPACE
// 모듈 등록 매크로
IMPLEMENT_MODULE(FMyGameModule, MyGameModule)
IMPLEMENT_MODULE- 일반 모듈용IMPLEMENT_GAME_MODULE- 게임 모듈용 (핫 리로드 지원)IMPLEMENT_PRIMARY_GAME_MODULE- 프로젝트의 주 게임 모듈
API 매크로와 심볼 내보내기
모듈 간 심볼 공유를 위한 API 매크로 사용법
API 매크로의 역할
API 매크로(예: MYGAME_API)는 DLL에서 심볼을 내보내거나 가져오는 데 사용됩니다. 다른 모듈에서 접근해야 하는 클래스에만 적용하세요.
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "MyPublicClass.generated.h"
// 다른 모듈에서 사용할 클래스 - API 매크로 필요
UCLASS()
class MYGAMEMODULE_API UMyPublicClass : public UObject
{
GENERATED_BODY()
public:
// 공개 함수 - 다른 모듈에서 호출 가능
UFUNCTION(BlueprintCallable, Category = "MyGame")
void PublicFunction();
// 공개 속성
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float PublicValue;
};
// 다른 모듈에서 사용할 구조체
USTRUCT(BlueprintType)
struct MYGAMEMODULE_API FMyPublicStruct
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FString Name;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 Value;
};
내부 전용 클래스 (API 매크로 없음)
#pragma once
#include "CoreMinimal.h"
// 모듈 내부에서만 사용 - API 매크로 없음
class FInternalHelper
{
public:
static void DoInternalWork();
static int32 CalculateSomething(int32 Input);
private:
static TArray<int32> CachedResults;
};
// 내부 전용 UObject 클래스
UCLASS()
class UInternalComponent : public UActorComponent
{
GENERATED_BODY()
// 모듈 내부에서만 사용하므로 API 매크로 불필요
};
모든 클래스에 API 매크로를 추가하지 마세요! 불필요한 심볼 내보내기는:
- DLL 크기 증가
- 링크 시간 증가
- 잠재적 ABI 호환성 문제
외부 모듈에서 실제로 필요한 클래스에만 적용하세요.
대규모 프로젝트의 모듈 구성
오픈월드 RPG 프로젝트를 위한 모듈 분리 전략
권장 모듈 구조
MyOpenWorldRPG/
├── Source/
│ ├── MyGameCore/ // 핵심 시스템 (런타임)
│ │ ├── Public/
│ │ │ ├── Interfaces/ // 공용 인터페이스
│ │ │ └── Types/ // 공용 타입/구조체
│ │ └── Private/
│ │
│ ├── MyGameplay/ // 게임플레이 시스템
│ │ ├── Combat/ // 전투 시스템
│ │ ├── Inventory/ // 인벤토리
│ │ └── Quest/ // 퀘스트
│ │
│ ├── MyGameUI/ // UI 전용 모듈
│ │ └── Widgets/
│ │
│ ├── MyGameAI/ // AI 전용 모듈
│ │ ├── BehaviorTree/
│ │ └── Perception/
│ │
│ └── MyGameEditor/ // 에디터 전용 모듈
│ └── Tools/
│
└── Plugins/
├── AbilitySystem/ // GAS 확장 플러그인
└── OpenWorldTools/ // 오픈월드 도구 플러그인
모듈 의존성 그래프
MyGameCore
기본 타입, 인터페이스, 유틸리티
의존성: Core, CoreUObject, Engine
MyGameplay
전투, 인벤토리, 퀘스트 시스템
의존성: MyGameCore, GAS
MyGameUI
HUD, 메뉴, 위젯
의존성: MyGameCore, UMG, Slate
MyGameAI
AI, Behavior Tree, Perception
의존성: MyGameCore, AIModule
.uproject 파일의 모듈 설정
{
"FileVersion": 3,
"EngineAssociation": "5.6",
"Category": "",
"Description": "",
"Modules": [
{
"Name": "MyGameCore",
"Type": "Runtime",
"LoadingPhase": "Default"
},
{
"Name": "MyGameplay",
"Type": "Runtime",
"LoadingPhase": "Default"
},
{
"Name": "MyGameUI",
"Type": "Runtime",
"LoadingPhase": "Default"
},
{
"Name": "MyGameAI",
"Type": "Runtime",
"LoadingPhase": "Default"
},
{
"Name": "MyGameEditor",
"Type": "Editor",
"LoadingPhase": "Default"
}
],
"Plugins": [
{
"Name": "GameplayAbilities",
"Enabled": true
}
]
}
핵심 요약
- 모듈은 관련 C++ 코드를 논리적 단위로 그룹화하며, 독립적 컴파일과 명시적 의존성 관리를 제공
- Build.cs 파일에서 PublicDependency(공개 API용)와 PrivateDependency(내부 구현용)를 구분
- Public/Private 폴더 분리로 API 캡슐화와 컴파일 시간 최적화 달성
- API 매크로는 외부 모듈에서 필요한 클래스에만 선택적으로 적용
- 대규모 프로젝트는 Core/Gameplay/UI/AI/Editor 등으로 모듈을 분리하여 관리
다음 강의에서는 플러그인 생성 방법과 .uplugin 설정, 모듈 타입(Runtime/Editor)의 차이점을 학습합니다.
도전 과제
배운 내용을 직접 실습해보세요
UE5 프로젝트의 .uproject 파일과 .Build.cs 파일을 분석하세요. PublicDependencyModuleNames, PrivateDependencyModuleNames의 차이를 이해하고, RPG 프로젝트의 모듈 의존성 그래프를 그리세요.
RPG 프로젝트에 별도의 GameModule(RPGCore, RPGCombat, RPGUI)을 생성하세요. 각 모듈의 Build.cs를 작성하고, IModuleInterface의 StartupModule()/ShutdownModule()을 구현하세요.
RPGCore 모듈의 인터페이스만 RPGCombat과 RPGUI에 공개하는 구조를 설계하세요. Public/Private 폴더를 분리하고, MinimalAPI와 MYGAME_API 매크로를 전략적으로 사용하세요.