IModuleInterface, StartupModule, ShutdownModule
7강. IModuleInterface, StartupModule, ShutdownModule
1. 이번 강의에서 해결할 문제
플러그인을 다시 컴파일하거나 비활성화한 뒤 메뉴가 중복되고, 종료 중 이미 내려간 Editor 모듈을 다시 Load해 크래시가 발생합니다. StartupModule은 전역 등록을 시작하는 곳이고 ShutdownModule은 등록 순서를 거꾸로 되돌리는 곳이어야 합니다.
2. 학습 목표
3. 핵심 개념
StartupModule은 Binary가 로드된 뒤 호출되며 등록, Factory 생성, Console Command와 Delegate 연결에 적합합니다. UObject World가 준비됐다고 가정하는 게임 초기화 함수가 아닙니다. ShutdownModule은 정상 종료, 모듈 Unload와 일부 Reload 경로에서 호출될 수 있습니다.
모듈 수명 용어와 실행 흐름
| 용어 | 의미 | 주의할 점 |
|---|---|---|
| Module Binary | Module 코드를 담은 DLL 또는 플랫폼별 Binary | Target과 Loading Phase에 따라 프로세스에 로드됩니다. |
| Module Object | IModuleInterface 구현 인스턴스 | ModuleManager가 Binary 수명과 함께 생성·파괴합니다. |
| 외부 등록 | 다른 시스템이 Module 함수·객체를 나중에 호출하도록 연결하는 것 | Delegate, Menu, Tab, Customization을 반드시 추적합니다. |
| Delegate Handle | 등록한 Callback 한 개를 다시 찾는 식별자 | 지역 변수로 버리면 정확한 해제가 어려워집니다. |
| Registration Owner | 여러 메뉴 Entry처럼 한 소유자가 등록한 항목 묶음 | Reload 때 기존 묶음을 한 번에 제거하는 데 사용합니다. |
ModuleManager가 Binary Load
→ Module Object 생성
→ StartupModule 호출
→ 외부 Delegate·Menu에 Callback 등록
→ Editor가 나중에 Callback 호출
→ ShutdownModule에서 등록 역순 해제
→ Module Object와 Binary Unload
핵심 위험은 외부 시스템이 Module보다 오래 사는 경우입니다. Module이 Unload된 뒤 Raw Delegate가 남으면 이미 사라진 코드 주소를 호출할 수 있습니다. 따라서 등록과 해제는 기능상 선택이 아니라 메모리 안전과 Reload 안정성의 일부입니다.
Module이 소유한 외부 등록은 다음처럼 추적합니다.
| 등록 대상 | 보관할 것 | 해제 방법 |
|---|---|---|
| Engine Delegate | FDelegateHandle | 원본 Delegate에서 Remove |
| ToolMenus Startup | Callback Handle 또는 Owner | Callback 해제와 Owner 제거 |
| Nomad Tab | Tab ID | UnregisterNomadTabSpawner |
| Property Customization | Class/Struct 이름 | PropertyEditor에서 Unregister |
4. 단계별 실습
#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"
#include "ToolMenus.h"
class FDataGuardEditorModule final : public IModuleInterface
{
public:
virtual void StartupModule() override
{
UE_LOG(LogTemp, Display, TEXT("[DataGuardEditor] StartupModule"));
ToolMenusHandle = UToolMenus::RegisterStartupCallback(
FSimpleMulticastDelegate::FDelegate::CreateRaw(
this, &FDataGuardEditorModule::RegisterMenus));
}
virtual void ShutdownModule() override
{
UE_LOG(LogTemp, Display, TEXT("[DataGuardEditor] ShutdownModule"));
UToolMenus::UnRegisterStartupCallback(ToolMenusHandle);
if (UObjectInitialized() && UToolMenus::IsToolMenuUIEnabled())
{
UToolMenus::UnregisterOwner(this);
}
}
private:
void RegisterMenus()
{
UE_LOG(LogTemp, Display, TEXT("[DataGuardEditor] RegisterMenus"));
FToolMenuOwnerScoped OwnerScoped(this);
// 22강에서 실제 메뉴 Entry를 등록합니다.
}
FDelegateHandle ToolMenusHandle;
};
IMPLEMENT_MODULE(FDataGuardEditorModule, DataGuardEditor)
코드 한 줄씩 이해하기
IModuleInterface는 게임 Actor 수명이 아니라 Module Load·Unload Hook을 제공합니다.RegisterStartupCallback은 ToolMenus가 준비된 시점에RegisterMenus를 호출하도록 예약합니다. StartupModule 호출과 실제 메뉴 등록 시점이 같다고 가정하지 않습니다.CreateRaw(this, ...)는 Module 객체의 생명에 자동 추적되는 약한 참조가 아닙니다. Shutdown에서 Callback을 반드시 제거해야 합니다.ToolMenusHandle을 Module 멤버에 저장해 등록한 정확한 Callback을 해제합니다.FToolMenuOwnerScoped는 RegisterMenus 안에서 추가되는 Entry의 소유자를 이 Module로 표시합니다.- Shutdown은 Callback 예약을 제거한 뒤, ToolMenus가 아직 사용 가능한 경우 Owner가 등록한 항목을 제거합니다.
IMPLEMENT_MODULE은 ModuleManager가DataGuardEditor이름으로 구현 객체를 만들 Entry Point를 제공합니다. Descriptor와 Build.cs의 Module 이름도 일치해야 합니다.
엔진 버전에 따라 ToolMenus API 이름과 종료 가능 여부가 달라질 수 있습니다. 현재 엔진 Header와 동일 버전의 공식 API를 확인하고, 종료 중 이미 내려간 모듈을 LoadModuleChecked로 되살려 해제하려 하지 않습니다.
엔진 버전에 따라 ToolMenus의 종료 가드나 API 이름이 달라질 수 있으므로 현재 Engine Header의 구현을 확인합니다. 핵심은 등록 Owner와 Handle을 Module 멤버로 보존하고 Shutdown에서 중복 없이 제거하는 것입니다.
Runtime Module이 등록할 것이 없다면 다음처럼 기본 구현을 사용해도 됩니다.
#include "Modules/ModuleManager.h"
IMPLEMENT_MODULE(FDefaultModuleImpl, ReusableInteraction)
5. 코드가 동작하는 이유
Module 객체는 ModuleManager가 Binary 수명과 함께 관리합니다. Raw Delegate가 Module 객체를 가리키는 동안 Callback을 남기면 Unload 뒤 잘못된 주소를 호출할 수 있습니다. Shutdown에서 외부 등록을 제거하면 Module 코드가 사라진 뒤 호출되는 참조를 차단합니다.
6. 자주 하는 실수와 해결법
7. 직접 실습
실습 목표
Module Load → Callback 등록 → Callback 실행 → 등록 해제 순서를 로그로 확인하고 Reload 뒤 중복 등록이 없는지 검증합니다.
시작 전 상태
DataGuardEditor가 Editor 전용 Module로 등록돼 Development Editor Target이 빌드되는지 확인합니다. Output Log 필터에 DataGuardEditor를 입력하고 Editor를 완전히 종료할 수 있는 작업 상태를 저장합니다.
1단계: 따라 하기
Editor를 시작해 StartupModule과 RegisterMenus 로그의 순서를 확인합니다. Startup은 한 번, ToolMenus 준비 뒤 RegisterMenus도 한 번이어야 합니다. 22강 전이라 실제 Entry가 없어도 Callback 실행 로그로 수명을 확인할 수 있습니다.
2단계: 값 바꿔 보기
RegisterMenus 로그를 임시 카운터와 함께 출력해 Editor 재시작 전후 각 프로세스에서 1부터 시작하는지 확인합니다. Handle 저장이나 Owner 제거를 임시로 누락한 실패 사례는 별도 실습 브랜치에서만 재현하고 즉시 복원합니다.
3단계: 직접 적용
Module이 추가로 등록하는 Engine Delegate 하나를 골라 FDelegateHandle 멤버에 저장하고 Shutdown에서 제거하세요. 등록 표에 등록 API, Handle 또는 Owner, 해제 API, 의존 Module 네 열을 기록합니다.
4단계: 스스로 확인
막혔을 때
| 증상 | 원인 후보 | 확인과 해결 |
|---|---|---|
| Startup 로그가 없음 | Descriptor Module 이름·Type·LoadingPhase 또는 빌드 실패 | .uplugin, Build.cs, IMPLEMENT_MODULE 이름을 맞추고 Module 로드 로그를 봅니다. |
| RegisterMenus가 여러 번 호출 | Startup Callback 중복 또는 이전 Owner 잔존 | 저장한 Handle과 Owner의 대칭 해제를 확인합니다. |
| Editor 종료 중 크래시 | 이미 Unload된 의존 Module 접근 | IsModuleLoaded와 엔진 종료 상태를 확인하고 강제 Load를 제거합니다. |
| Live Coding에서 Shutdown 로그가 없음 | 해당 변경이 Module Unload를 수행하지 않음 | Live Coding 결과만으로 수명 검증을 끝내지 말고 Editor 완전 재시작을 사용합니다. |
8. 이해 점검 질문 3개
9. 핵심 요약
다음 강의 연결
다음 강의: Module 로딩 단계와 LoadingPhase에서는 StartupModule이 호출되는 시점을 기능이 실제로 필요해지는 엔진 초기화 단계와 맞춥니다.
IModuleInterface, StartupModule, ShutdownModule 미니 퀴즈
선택 즉시 정답과 해설을 확인할 수 있습니다. 결과는 이 브라우저에만 저장됩니다.
학습을 마쳤나요?
직접 실습과 점검 질문까지 확인한 뒤 완료로 표시하세요.