UE5 모듈 Public/Private 폴더 구조 이전
UBT가 include path에 넣는 건 Public 루트뿐
카테고리 폴더에 헤더와 소스를 함께 쌓으면 어떤 헤더가 모듈 밖으로 공개되는지 구조에서 안 보인다. 언리얼 표준 구조로 옮기며 include 경로와 빌드가 깨진 지점들.
모듈 안 파일이 늘어나면서 헤더와 소스가 카테고리 폴더에 뒤섞여 쌓였다. 당장은 굴러가지만, 어떤 헤더가 모듈 밖으로 공개되는지가 폴더 구조에서 드러나지 않는다는 게 문제였다. 이 글에서는 그 구조를 언리얼 표준인 Public/Private 분리로 옮긴 과정을 이야기하려 한다 — 왜 언리얼이 이 구조를 강제하는지, 이전 중 include 경로와 빌드가 어떻게 깨졌고 어떻게 복구했는지다.
폴더 구조 이전 — 평면 카테고리에서 Public/Private로
앞선 결론은 “중첩 Combat/Public + Combat/Private은 UBT 자동 인식이 안 되니 flat 구조로 회복”이었다. 그 결론 자체는 맞지만 한 단계 더 가야 할 곳이 있었다 — 모듈 루트 단일 Public/Private + 카테고리 서브폴더 구조. 앞의 것이 회피책이라면 이번은 정식 표준으로의 이전이다.
앞선 시행착오 — “중첩 Public/Private은 동작 안 한다”는 결론에서 출발
먼저 두 결론의 관계를 정리.
1
2
3
4
5
6
7
8
9
이전 구조
✗ 시도 1: Module/Combat/Public + Module/Combat/Private (중첩)
→ 빌드 실패 (UBT는 모듈 루트의 Public/Private만 자동 인식)
✓ 회복: Module/Combat/{*.h, *.cpp} (flat)
→ 빌드 성공. 단 Public/Private 분리 없음
이번 구조
✓ 정식: Module/Public/Combat/*.h + Module/Private/Combat/*.cpp
→ 빌드 성공 + Public/Private 분리 + 카테고리 보존
세 구조의 차이가 한눈에 보인다. 앞서 회복한 flat은 빌드는 되지만 API 경계가 흐려진다 — 외부에서 가져갈 헤더와 내부 구현을 폴더 단위로 구분 못 함. 이번 패턴은 두 가지를 동시에 만족한다.
참조 모델 — VoidUnreal 패턴의 미묘한 포인트
기준 자료는 이전에 동작을 검증해 둔 프로젝트의 모듈 구조다. 핵심 패턴 4가지.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
VoidUnreal/Source/VoidUnreal/
├── VoidUnreal.Build.cs ← 모듈 빌드 설정
├── VoidUnreal.cpp ← 모듈.cpp는 모듈 루트
├── Public/
│ ├── VoidUnreal.h ← 모듈.h는 Public 루트
│ ├── AI/
│ │ ├── VOIDAIController.h
│ │ └── ...
│ ├── Combat/
│ │ ├── HealthComponent.h
│ │ ├── WeaponComponent.h
│ │ └── ...
│ └── Player/
│ └── VOIDPlayerController.h
└── Private/
├── AI/
│ └── VOIDAIController.cpp
├── Combat/
│ └── HealthComponent.cpp
└── Player/
└── VOIDPlayerController.cpp
미묘한 포인트:
- 모듈.h는 Public 루트에, 모듈.cpp는 모듈 루트에 있다 (Private/ 안이 아님). 이건 UE5 템플릿이 만들어주는 그대로의 위치
- 카테고리(AI/Combat/Player)는 Public·Private 양쪽에 대칭으로 같은 이름이 존재
Public/,Private/외 다른 모든 것(Build.cs, 모듈 cpp 등)은 모듈 루트 직속
이 4가지를 그대로 따라야 UBT 자동 인식이 정상 동작.
이행 결과 — 27개 파일 이동, 19개 파일 include 경로 수정
NBC_Ch3 모듈을 위 패턴으로 정렬.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
이전 (카테고리별 평면)
NBC_Ch3/Source/NBC_Ch3_TeamProject/
├── AI/ ← .h와 .cpp 한 폴더에 섞임
├── Combat/
├── Core/
├── Player/
├── System/
├── UI/
└── (모듈 파일들 평면 배치)
이후 (Public/Private + 카테고리 서브폴더)
NBC_Ch3/Source/NBC_Ch3_TeamProject/
├── NBC_Ch3_TeamProject.Build.cs
├── NBC_Ch3_TeamProject.cpp ← 모듈 cpp는 루트
├── Public/
│ ├── NBC_Ch3_TeamProject.h ← 모듈 h는 Public 루트
│ ├── AI/*.h
│ ├── Combat/*.h
│ ├── Core/*.h
│ ├── Player/*.h
│ ├── System/*.h
│ └── UI/*.h
└── Private/
├── AI/*.cpp
├── Combat/*.cpp
├── Core/*.cpp
├── Player/*.cpp
├── System/*.cpp
└── UI/*.cpp
이동 통계:
| 분류 | 개수 | 비고 |
|---|---|---|
| 헤더(.h) | 13 | Public/{Category}/ 로 이동 |
| 구현(.cpp) | 12 | Private/{Category}/ 로 이동 |
| 모듈 파일 | 2 | 모듈.h → Public 루트 / 모듈.cpp → 모듈 루트 |
| OWNER.txt | 6 | Public/{Category}/ 로 이동(아래 별도 절) |
| 합계 | 27 | 모두 git mv 사용 — 히스토리 보존 |
| include 경로 수정 | 19개 파일 | #include "Foo.h" → #include "Category/Foo.h" |
git mv 를 쓴 이유는 단순 mv 후 git add 하면 Git이 100% 유사도로 자동 추적해 주긴 하지만, 명시적 rename이 PR diff에서 더 명확하기 때문. git log --follow 결과도 더 안정적.
UBT가 자동으로 추가하는 include path
이 작업에서 가장 중요한 한 줄.
UnrealBuildTool은 모듈의
Public/루트만 자동으로 include path에 추가한다.Public/AI/같은 서브폴더는 추가하지 않는다.
이게 두 구조 결정의 핵심 원리다.
1
2
3
4
5
6
7
8
9
10
모듈 = NBC_Ch3_TeamProject
UBT가 자동 추가하는 PublicIncludePaths:
✓ NBC_Ch3_TeamProject/Public/ ← 자동
✗ NBC_Ch3_TeamProject/Public/AI/ ← 자동 X
✗ NBC_Ch3_TeamProject/Public/Combat/ ← 자동 X
UBT가 자동 추가하는 PrivateIncludePaths:
✓ NBC_Ch3_TeamProject/Private/ ← 자동
✗ NBC_Ch3_TeamProject/Private/AI/ ← 자동 X
따라서 같은 모듈 안에서 Combat/HealthComponent.h 를 가져오려면 반드시 #include "Combat/HealthComponent.h" 처럼 카테고리 prefix 풀 경로를 써야 한다. #include "HealthComponent.h" 만 쓰면 컴파일러가 못 찾음.
같은 모듈 내부 헤더 참조도 풀 경로 — 모던 UE5 스타일
이게 19개 파일을 수정한 이유. 이전 평면 구조에서는 모든 .h가 같은 모듈 루트 직속 카테고리 폴더 안에 있어서 #include "Foo.h" 만으로 충분했는데(UBT가 모듈 루트 하위 모든 폴더를 스캔해 주는 레거시 모드), 이제는 Public/Foo.h 또는 Public/Category/Foo.h 형태라 카테고리 prefix 가 필수.
1
2
3
4
5
6
7
8
9
// Before (평면 구조)
#include "HealthComponent.h"
#include "ZombieAIController.h"
#include "PlayerController.h"
// After (Public/Category 구조)
#include "Combat/HealthComponent.h"
#include "AI/ZombieAIController.h"
#include "Player/PlayerController.h"
수정 패턴은 단순 — 헤더가 어느 카테고리에 속하는지 보고 prefix 추가. 자동화 가능한 작업이지만 19개 파일 정도는 직접 확인하면서 했다(외부 모듈 헤더와 내부 헤더 구분 위해).
bLegacyPublicIncludePaths — 끄는 게 표준
UE5에는 빌드 옵션 하나가 있다 — bLegacyPublicIncludePaths. 이걸 true 로 두면 UBT가 Public/ 하위 모든 서브폴더도 자동으로 include path에 추가해 준다. 그러면 #include "HealthComponent.h" 같은 짧은 경로도 동작.
1
2
3
4
5
6
7
8
9
10
11
12
// Build.cs
public class NBC_Ch3_TeamProject : ModuleRules
{
public NBC_Ch3_TeamProject(ReadOnlyTargetRules Target) : base(Target)
{
// (1) Legacy 모드 — true 시 Public 하위 서브폴더도 자동 인식
bLegacyPublicIncludePaths = true;
// (2) 모던 모드 — false (또는 미설정 = 기본값 false)
// bLegacyPublicIncludePaths = false;
}
}
VoidUnreal 검증 결과 — 미설정 = false (모던 모드) 가 표준. 이유:
- legacy 모드는 UBT 가 모든 서브폴더를 스캔해 include path에 등록 → 빌드 시간 증가
- 짧은 경로는 모듈 간 헤더 충돌 가능성 ↑ (예: 두 모듈에
Foo.h가 있으면 어느 게 잡힐지 모호) - 풀 경로(
Combat/Foo.h)가 의도가 명확
NBC_Ch3 도 미설정으로 두고 모든 include를 풀 경로로 통일.
다른 모듈에서 가져갈 때 — Build.cs 의존성 + 동일 풀 경로
같은 모듈 내부 참조 규칙은 위와 같고, 다른 모듈에서 우리 헤더를 가져갈 때도 정확히 같은 형식.
1
2
3
4
5
// 가져가는 쪽 모듈의 Build.cs
PublicDependencyModuleNames.AddRange(new string[] {
"Core", "CoreUObject", "Engine", "InputCore",
"NBC_Ch3_TeamProject" // ← 우리 모듈을 의존성으로 추가
});
1
2
// 가져가는 쪽 .cpp
#include "Combat/HealthComponent.h" // ← 우리 모듈의 풀 경로 그대로
같은 풀 경로가 모듈 내부·외부 양쪽에서 통한다 — 이게 모던 UE5의 깔끔한 설계. legacy 모드면 모듈 외부에서는 풀 경로가 강제이고 모듈 내부에서는 짧은 경로가 동작해 일관성이 깨졌다.
OWNER.txt 같은 비코드 파일도 카테고리 따라 이동
발견한 디테일 — 각 카테고리 폴더에 OWNER.txt (담당자 표시) 가 있었다. 코드 파일은 아니지만 카테고리에 종속되는 메타데이터.
1
2
이전: NBC_Ch3_TeamProject/Combat/OWNER.txt
이후: NBC_Ch3_TeamProject/Public/Combat/OWNER.txt
선택지 두 가지 중 Public 쪽으로 결정:
- (A) Public/Category/ 에 두기 — 외부에서 “이 카테고리 소유자가 누구야”를 볼 때 헤더와 함께 보이는 게 자연스러움. 선택
- (B) Private/Category/ 에 두기 — 구현 디렉터리에 같이. 외부에서 안 보이게
OWNER.txt는 코드가 아니라 카테고리 자체의 메타 라 6개 모두 Public 쪽 카테고리 폴더로 이동. 모듈 사용자가 헤더 보러 들어왔을 때 같이 눈에 띈다.
이 결정에서 일반화한 룰: 비코드 파일이라도 카테고리에 종속되는 메타데이터는 카테고리 폴더 안에 함께 두고, Public/Private 분리 시에도 카테고리를 단위로 따라간다.
정리 — 폴더 구조 이전에서 남은 것
모듈 구조 쪽. UE5 모듈의 정식 표준은 Module/Public/{Category}/*.h + Module/Private/{Category}/*.cpp다. 앞서 회복한 flat 구조도 빌드는 되지만, 외부에 공개할 헤더와 내부 구현이 폴더로 구분되지 않아 API 경계가 흐려진다. 원리는 한 줄로 정리된다 — UBT가 include path에 자동 추가하는 건 모듈의 Public/ 루트뿐이라, 같은 모듈 안에서도 #include "Category/Foo.h" 풀 경로를 써야 하고, 다른 모듈에서 가져갈 때도 Build.cs 의존성만 추가하면 같은 경로가 그대로 통한다. bLegacyPublicIncludePaths를 켜면 짧은 경로도 동작하지만 빌드 시간이 늘고 헤더 이름이 충돌할 여지가 생기니, 기본값(끔) 그대로 두는 게 표준이다.
작업 디테일 두 가지도 남겨 둔다. 파일 이동은 git mv로 했다 — 단순 mv 후 add 해도 Git이 rename을 추적해 주긴 하지만, 명시적 rename이 PR diff 가독성과 git log --follow 안정성에서 낫다. OWNER.txt처럼 코드가 아닌 파일도 카테고리에 종속되는 메타데이터라면 카테고리 폴더를 따라 같이 옮기는 게 자연스러웠다.
PR 워크플로우 전수에서 확인한 것. 결국 “1 작업 = 1 브랜치 = 1 PR”과 “PR 본문에 무엇을/왜/어떻게 검증했는지 적기” 두 가지만 지켜도 협업 대부분이 정상 궤도에 오른다. 명령어를 외우게 하기보다 왜 main 직접 push가 위험한지 같은 의도를 먼저 설명했고, 자주 하는 실수 4가지(main 직접 push / pull 없이 작업 / 너무 큰 PR / 커밋 메시지 불일관)는 main 보호 규칙 + Conventional Commits + PR 단위 쪼개기 합의로 예방된다는 것까지 묶어서 전달했다. CLI만으로 가르치면 명령어 암기에서 막히니 GUI(GitHub Desktop 등)로 시작해 익숙해지면 CLI로 넘어가는 순서를 권했고, 우리 팀 develop에 실제 머지된 PR 3건을 화면으로 같이 보면서 설명하니 추상적인 룰 설명보다 훨씬 빨리 이해했다.
핵심 요약 — UBT는 모듈의
Public/루트만 include path에 자동 추가한다. 그래서 Public/Private + 카테고리 서브폴더 구조에서는 모듈 안팎 모두#include "Category/Foo.h"풀 경로 한 가지로 통일된다.