Unity·Unreal 실전

Unreal Engine C++ Data Validation: UEditorValidator로 에셋 오류와 빌드 크래시 사전 방지하기

kr-gamedev 2026. 6. 25. 09:57

게임 프로젝트가 커질수록 다루는 에셋의 양은 빠르게 불어납니다. 기획자, 아티스트, 프로그래머가 매일 수많은 캐릭터 모델, 텍스처, 사운드, 데이터 에셋을 에디터로 임포트하고 수정하죠. 이 과정에서 밉맵(Mipmap) 설정이 빠진 큰 텍스처가 들어오거나, LOD(Level of Detail)가 하나도 없는 무거운 스태틱 메시가 배치되는 실수가 자주 생깁니다.

이런 작은 세팅 실수는 단순히 빌드 용량을 키우는 데 그치지 않습니다. 밉맵이 없어 텍스처 메모리가 폭증하거나, 잘못 구성된 데이터 에셋이 런타임에서 널 포인터 참조 크래시로 이어지기도 합니다. 최악의 경우 패키징 단계의 쿡(Cook) 과정에서 에러가 나면서 전체 빌드 파이프라인이 멈춰 서기도 합니다.

에셋 임포트 가이드를 문서로 배포하는 것만으로는 이런 실수를 완벽히 막을 수 없습니다. 사람의 실수를 원천 차단하는 가장 좋은 방법은, 에셋을 저장하는 시점이나 패키징에 들어가기 전에 엔진이 직접 규칙 위반을 검사하고 경고나 에러를 띄우는 것입니다. 언리얼 엔진 5(UE5)는 이를 위한 C++ 에셋 검증 프레임워크인 Data ValidationUEditorValidatorBase를 제공합니다.

참고: 이 글은 Unreal Engine 5.8 기준입니다. 아래 C++ 시그니처는 UE 5.4에서 한 차례 바뀐 형태이며, 그 이후 최신 5.8까지 동일하게 유지됩니다. 5.3 이하 예제를 그대로 가져오면 컴파일이 깨질 수 있으니 주의하세요.


1. Data Validation 시스템의 동작 원리

언리얼 엔진 5의 UEditorValidatorSubsystem은 에디터에서 파일을 저장(Save)하거나, 콘텐츠 브라우저에서 사용자가 직접 검증을 요청할 때, 등록된 검증 규칙들을 차례로 실행합니다.

커스텀 검증기는 UEditorValidatorBase를 상속하는 C++ 클래스로 만듭니다. 핵심은 두 개의 함수입니다.

  1. CanValidateAsset_Implementation: 전달된 에셋이 이 검증기의 대상(예: 특정 텍스처, 캐릭터 데이터 에셋 등)인지 판별합니다.
  2. ValidateLoadedAsset_Implementation: 에셋의 프로퍼티를 직접 읽어 규칙 위반을 검사하고, 결과를 AssetPasses 또는 AssetFails로 보고합니다.

빌드 파이프라인(CI/CD) 연동도 간단합니다. 언리얼 에디터 커맨드릿(Commandlet)으로 데이터 검증을 따로 돌릴 수 있어서, 잘못된 에셋이 저장소에 머무는 것을 빌드 이전 단계에서 잡아낼 수 있습니다.


2. UE 5.4에서 바뀐 함수 시그니처 (중요)

여기가 가장 헷갈리는 지점입니다. UE 5.3 이하에서 흔히 쓰던 시그니처는 이랬습니다.

// UE 5.3 이하 — 현재는 deprecated
virtual bool CanValidateAsset_Implementation(UObject* InAsset) const override;

UE 5.4부터는 검증 컨텍스트와 에셋 메타데이터를 함께 받도록 인자가 추가됐고, 이 형태가 최신 5.8까지 그대로 권장됩니다. 옛 1개 인자 버전은 deprecated 처리됐습니다.

// UE 5.4 ~ 5.8 — 권장 시그니처
virtual bool CanValidateAsset_Implementation(
    const FAssetData& InAssetData,
    UObject* InObject,
    FDataValidationContext& InContext) const override;

핵심은 두 가지입니다.

  • CanValidateAsset_Implementation은 이제 FAssetData, UObject*, FDataValidationContext&를 함께 받으며 const입니다. 덕분에 에셋을 메모리에 완전히 로드하지 않고도 FAssetData의 가벼운 메타데이터(클래스 타입 등)만으로 검증 대상 여부를 빠르게 판단할 수 있습니다.
  • ValidateLoadedAsset_Implementation은 모든 실행 경로에서 반드시 AssetPasses 또는 AssetFails를 호출해야 합니다. 이 둘을 빼먹으면 검증 결과가 불완전하게 보고됩니다.

엔진은 하위 호환을 위해 옛 시그니처도 당분간 유지하지만, 새 코드는 위의 최신(5.4~5.8) 시그니처로 작성하는 것이 안전합니다.


3. 모듈 의존성 설정 (Build.cs)

에셋 검증 로직은 에디터에서만 동작하는 툴링 영역이므로, 에디터 전용 모듈(MyProjectEditor)에 구현하는 것이 기본입니다. 에디터 모듈의 Build.csDataValidationUnrealEd 모듈 의존성을 추가합니다.

// Source/MyProjectEditor.Build.cs
using UnrealBuildTool;

public class MyProjectEditor : ModuleRules
{
    public MyProjectEditor(ReadOnlyTargetRules Target) : base(Target)
    {
        PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

        PublicDependencyModuleNames.AddRange(new string[]
        {
            "Core", "CoreUObject", "Engine"
        });

        // 에디터 빌드 전용 모듈
        PrivateDependencyModuleNames.AddRange(new string[]
        {
            "UnrealEd",
            "DataValidation"
        });
    }
}

에디터 전용 모듈은 보통 .uproject / .Target.cs에서 Type=Editor로 분리해 두므로, 위처럼 의존성을 바로 추가해도 됩니다. 게임 런타임 모듈에 섞어 쓰는 경우라면 if (Target.bBuildEditor) { ... } 가드로 감싸 패키징 빌드에서 제외하면 됩니다.


4. C++ 커스텀 검증기 구현

아래 예제는 에셋이 저장되거나 검증이 요청될 때, 텍스처와 스태틱 메시를 대상으로 다음 규칙을 검사합니다.

  • 텍스처 해상도가 4096px을 초과하는지
  • UI 텍스처가 아닌데 2의 거듭제곱(Power of Two) 크기가 아닌지 (밉맵 생성 불가 → 메모리 이슈)
  • 스태틱 메시의 LOD 개수가 최소 2개 이상인지

헤더 파일 (MyAssetValidator.h)

#pragma once

#include "CoreMinimal.h"
#include "EditorValidatorBase.h"
#include "MyAssetValidator.generated.h"

/**
 * 프로젝트 전반의 에셋 세팅을 검사하는 커스텀 에디터 검증기 (UE 5.8 / 5.4+ 시그니처)
 */
UCLASS()
class MYPROJECTEDITOR_API UMyAssetValidator : public UEditorValidatorBase
{
    GENERATED_BODY()

public:
    UMyAssetValidator();

protected:
    // 이 검증기가 해당 에셋을 검사할 수 있는지 판별 (UE 5.4~5.8: FAssetData + Context 인자)
    virtual bool CanValidateAsset_Implementation(
        const FAssetData& InAssetData,
        UObject* InObject,
        FDataValidationContext& InContext) const override;

    // 실제 검증 로직 수행
    virtual EDataValidationResult ValidateLoadedAsset_Implementation(
        const FAssetData& InAssetData,
        UObject* InAsset,
        FDataValidationContext& Context) override;
};

소스 파일 (MyAssetValidator.cpp)

#include "MyAssetValidator.h"
#include "Engine/Texture2D.h"
#include "Engine/StaticMesh.h"
#include "Misc/DataValidation.h"

#define LOCTEXT_NAMESPACE "MyAssetValidator"

UMyAssetValidator::UMyAssetValidator()
{
    // 에디터에서 이 검증기를 활성화
    bIsEnabled = true;
}

bool UMyAssetValidator::CanValidateAsset_Implementation(
    const FAssetData& InAssetData,
    UObject* InObject,
    FDataValidationContext& InContext) const
{
    // FAssetData 의 가벼운 메타데이터(클래스 경로)로 먼저 거른다.
    // 에셋을 완전히 로드하기 전이라 비용이 싸다.
    const UClass* AssetClass = InAssetData.GetClass();
    if (!AssetClass)
    {
        return false;
    }

    return AssetClass->IsChildOf(UTexture2D::StaticClass())
        || AssetClass->IsChildOf(UStaticMesh::StaticClass());
}

EDataValidationResult UMyAssetValidator::ValidateLoadedAsset_Implementation(
    const FAssetData& InAssetData,
    UObject* InAsset,
    FDataValidationContext& Context)
{
    bool bHasErrors = false;

    // 1. Texture2D 검증
    if (const UTexture2D* Texture = Cast<UTexture2D>(InAsset))
    {
        const int32 SizeX = Texture->GetSizeX();
        const int32 SizeY = Texture->GetSizeY();

        // 프로젝트 사양을 넘는 초고해상도 텍스처 차단
        if (SizeX > 4096 || SizeY > 4096)
        {
            Context.AddError(FText::Format(
                LOCTEXT("TextureTooLarge",
                    "텍스처 '{0}'의 해상도가 제한(4096)을 초과했습니다: ({1}x{2})"),
                FText::FromString(Texture->GetName()),
                FText::AsNumber(SizeX),
                FText::AsNumber(SizeY)));
            bHasErrors = true;
        }

        // UI 텍스처가 아닌데 2의 거듭제곱이 아니면 밉맵 생성이 막힌다 → 경고
        const bool bIsPowerOfTwo =
            FMath::IsPowerOfTwo(SizeX) && FMath::IsPowerOfTwo(SizeY);
        const bool bIsUI = Texture->LODGroup == TEXTUREGROUP_UI;

        if (!bIsPowerOfTwo && !bIsUI)
        {
            Context.AddWarning(FText::Format(
                LOCTEXT("TextureNotPOT",
                    "텍스처 '{0}'은 UI 에셋이 아닌데 2의 거듭제곱 크기가 아닙니다. "
                    "밉맵이 생성되지 않아 메모리 문제가 생길 수 있습니다."),
                FText::FromString(Texture->GetName())));
        }
    }
    // 2. StaticMesh 검증
    else if (const UStaticMesh* StaticMesh = Cast<UStaticMesh>(InAsset))
    {
        const int32 LODCount = StaticMesh->GetNumLODs();

        // 원거리 렌더링 비용을 줄이기 위한 최소 LOD 강제
        if (LODCount < 2)
        {
            Context.AddError(FText::Format(
                LOCTEXT("StaticMeshMissingLOD",
                    "스태틱 메시 '{0}'의 LOD 개수가 부족합니다: 현재 {1}개 (최소 2개 필요)"),
                FText::FromString(StaticMesh->GetName()),
                FText::AsNumber(LODCount)));
            bHasErrors = true;
        }
    }

    // 모든 실행 경로에서 AssetPasses / AssetFails 중 하나는 반드시 호출해야 한다.
    if (bHasErrors)
    {
        AssetFails(InAsset,
            LOCTEXT("AssetValidationFailed", "에셋 검증에 실패했습니다."));
        return EDataValidationResult::Invalid;
    }

    AssetPasses(InAsset);
    return EDataValidationResult::Valid;
}

#undef LOCTEXT_NAMESPACE

여기서 흔히 틀리는 두 부분을 짚고 넘어가겠습니다.

  • AssetFails에셋과 메시지, 두 개의 인자만 받습니다. 에러 목록을 따로 넘기지 않습니다. 개별 에러 내용은 이미 Context.AddError(...)로 컨텍스트에 쌓아 두었기 때문에, AssetFails에는 요약 메시지만 전달하면 됩니다.
  • 에러·경고는 FDataValidationContextAddError / AddWarning으로 누적합니다. 컨텍스트에서 에러 목록을 다시 꺼내 AssetFails에 넘길 필요는 없습니다.

5. 에디터 테스트와 CI/CD 활용

C++ 코드를 컴파일하고 에디터를 실행하면, UMyAssetValidator가 검증 서브시스템에 자동 등록되어 동작합니다.

에디터에서 직접 검증

콘텐츠 브라우저에서 텍스처나 메시 에셋을 우클릭하고 Asset Actions → Validate Assets 메뉴를 누르면 결과를 바로 확인할 수 있습니다.

  • 통과 시: 메시지 로그 창에 검증 성공 문구가 출력됩니다.
  • 실패 시: 에디터 우측 하단에 에러 알림이 뜨고, 메시지 로그에 사유가 상세히 표시됩니다.

CI 파이프라인 통합

에디터를 직접 열기 어려운 빌드 서버에서는, 커맨드릿으로 프로젝트 전체 에셋을 무인 검사할 수 있습니다.

# 데이터 검증 커맨드릿 실행 예시 (경로/플랫폼은 환경에 맞게 조정)
UnrealEditor-Cmd.exe "C:\PathToProject\MyGame.uproject" ^
    -run=DataValidation -unattended -nullrhi -nosplash

검증 중 AddError 판정을 받은 에셋이 하나라도 있으면 0이 아닌 종료 코드를 반환하므로, 젠킨스나 GitHub Actions에서 이 단계를 게이트로 걸어 잘못된 에셋이 릴리즈에 섞여 들어가는 것을 막을 수 있습니다.


요약

  • 에셋 관리 자동화: UEditorValidatorBase를 상속해 규칙을 코드로 박아두면 수작업 검토 부담이 크게 줄어듭니다.
  • 시그니처 주의: UE 5.4부터 CanValidateAsset_ImplementationFAssetData + UObject* + FDataValidationContext& 형태로 바뀌었고 최신 5.8까지 동일합니다. AssetFails는 에셋과 메시지 두 인자만 받습니다. 옛 예제를 그대로 가져오면 컴파일이 깨지니 버전을 꼭 확인하세요.
  • CI/CD 시너지: 빌드 서버의 쿡 단계 이전에 커맨드릿으로 검증을 돌려, 문제가 있는 에셋을 일찍 걸러내면 파이프라인 낭비를 줄일 수 있습니다.