PART 9 - 강의 1/4

모듈 구조 이해

Build.cs, 의존성 관리, Public/Private 분리의 핵심 개념

01

언리얼 엔진 모듈 시스템

UE5 프로젝트의 기본 구성 단위인 모듈의 개념과 구조

모듈이란?

언리얼 엔진에서 모듈(Module)은 관련된 C++ 코드를 논리적 단위로 그룹화한 것입니다. 각 모듈은 독립적으로 컴파일되며, 다른 모듈과의 의존성을 명시적으로 선언합니다.

모듈의 핵심 특징
  • 독립적인 컴파일 단위
  • 명시적 의존성 관리
  • 캡슐화된 API (Public/Private 분리)
  • 재사용 가능한 코드 패키지

모듈 디렉토리 구조

Directory Structure MyModule/ ├── MyModule.Build.cs // 빌드 설정 파일 ├── Public/ // 외부 모듈에 노출할 헤더 │ ├── MyModule.h // 모듈 헤더 │ └── MyClass.h // 공개 클래스 └── Private/ // 내부 전용 파일 ├── MyModule.cpp // 모듈 구현 ├── MyClass.cpp // 클래스 구현 └── InternalHelper.h // 내부 헬퍼 (비공개)
Public vs Private 분리 원칙
  • Public/ - 다른 모듈에서 #include 가능한 헤더만 배치
  • Private/ - 모듈 내부에서만 사용하는 모든 파일 (.h, .cpp)
  • 이 분리로 불필요한 의존성과 컴파일 시간을 줄일 수 있음
02

Build.cs 파일 작성

모듈의 빌드 설정과 의존성을 정의하는 핵심 파일

기본 Build.cs 구조

MyGameModule.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 설정

AdvancedModule.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 활용
  • 모듈 구조 재설계
03

모듈 구현 코드

IModuleInterface를 상속받아 모듈의 시작/종료를 관리

모듈 헤더 파일

MyGameModule.h #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; };

모듈 구현 파일

MyGameModule.cpp #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 vs IMPLEMENT_GAME_MODULE
  • IMPLEMENT_MODULE - 일반 모듈용
  • IMPLEMENT_GAME_MODULE - 게임 모듈용 (핫 리로드 지원)
  • IMPLEMENT_PRIMARY_GAME_MODULE - 프로젝트의 주 게임 모듈
04

API 매크로와 심볼 내보내기

모듈 간 심볼 공유를 위한 API 매크로 사용법

API 매크로의 역할

API 매크로(예: MYGAME_API)는 DLL에서 심볼을 내보내거나 가져오는 데 사용됩니다. 다른 모듈에서 접근해야 하는 클래스에만 적용하세요.

Public/MyPublicClass.h #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 매크로 없음)

Private/InternalHelper.h #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 매크로 사용 시 주의

모든 클래스에 API 매크로를 추가하지 마세요! 불필요한 심볼 내보내기는:

  • DLL 크기 증가
  • 링크 시간 증가
  • 잠재적 ABI 호환성 문제

외부 모듈에서 실제로 필요한 클래스에만 적용하세요.

05

대규모 프로젝트의 모듈 구성

오픈월드 RPG 프로젝트를 위한 모듈 분리 전략

권장 모듈 구조

Project Module Structure 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 파일의 모듈 설정

MyOpenWorldRPG.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 } ] }
SUMMARY

핵심 요약

  • 모듈은 관련 C++ 코드를 논리적 단위로 그룹화하며, 독립적 컴파일과 명시적 의존성 관리를 제공
  • Build.cs 파일에서 PublicDependency(공개 API용)와 PrivateDependency(내부 구현용)를 구분
  • Public/Private 폴더 분리로 API 캡슐화와 컴파일 시간 최적화 달성
  • API 매크로는 외부 모듈에서 필요한 클래스에만 선택적으로 적용
  • 대규모 프로젝트는 Core/Gameplay/UI/AI/Editor 등으로 모듈을 분리하여 관리
다음 강의 예고

다음 강의에서는 플러그인 생성 방법과 .uplugin 설정, 모듈 타입(Runtime/Editor)의 차이점을 학습합니다.

PRACTICE

도전 과제

배운 내용을 직접 실습해보세요

실습 1: 모듈 구조 분석

UE5 프로젝트의 .uproject 파일과 .Build.cs 파일을 분석하세요. PublicDependencyModuleNames, PrivateDependencyModuleNames의 차이를 이해하고, RPG 프로젝트의 모듈 의존성 그래프를 그리세요.

실습 2: 커스텀 모듈 생성

RPG 프로젝트에 별도의 GameModule(RPGCore, RPGCombat, RPGUI)을 생성하세요. 각 모듈의 Build.cs를 작성하고, IModuleInterface의 StartupModule()/ShutdownModule()을 구현하세요.

심화 과제: 모듈 간 인터페이스 설계

RPGCore 모듈의 인터페이스만 RPGCombat과 RPGUI에 공개하는 구조를 설계하세요. Public/Private 폴더를 분리하고, MinimalAPI와 MYGAME_API 매크로를 전략적으로 사용하세요.