포스트

UE5 모듈 Public/Private 폴더 구조 이전

UBT가 include path에 넣는 건 Public 루트뿐

카테고리 폴더에 헤더와 소스를 함께 쌓으면 어떤 헤더가 모듈 밖으로 공개되는지 구조에서 안 보인다. 언리얼 표준 구조로 옮기며 include 경로와 빌드가 깨진 지점들.

UE5 모듈 Public/Private 폴더 구조 이전

모듈 안 파일이 늘어나면서 헤더와 소스가 카테고리 폴더에 뒤섞여 쌓였다. 당장은 굴러가지만, 어떤 헤더가 모듈 밖으로 공개되는지가 폴더 구조에서 드러나지 않는다는 게 문제였다. 이 글에서는 그 구조를 언리얼 표준인 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)13Public/{Category}/ 로 이동
구현(.cpp)12Private/{Category}/ 로 이동
모듈 파일2모듈.h → Public 루트 / 모듈.cpp → 모듈 루트
OWNER.txt6Public/{Category}/ 로 이동(아래 별도 절)
합계27모두 git mv 사용 — 히스토리 보존
include 경로 수정19개 파일#include "Foo.h"#include "Category/Foo.h"

git mv 를 쓴 이유는 단순 mvgit 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" 풀 경로 한 가지로 통일된다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.