Unity 검색

개선된 메시징으로 API 사용 요구사항 명확히 하기

2022년 9월 12일 테크놀로지 | 9 분 소요
Unity stock image of developer at work with light-up headphones
Unity stock image of developer at work with light-up headphones

어떤 조건에서 API가 호출될 수 있는지 또는 호출되어야 하는지 불분명할 때가 있습니다. 이러한 정보를 곧바로 얻지 못하면 의도하지 않은 부작용이 수반되는 코드를 작성하게 됩니다. 

Unity 에디터의 사용 사례가 증가한 만큼 API를 사용하는 방식도 다양해졌습니다. 초기화하지 않은 시스템을 API가 직접 사용하려 하면 시스템이 불안정해질 수 있습니다. 씬을 여는 동안 해당 씬을 삭제하거나, 임포트 중에 에셋을 로드하려고 하면 일관적인 결과가 도출되지 않을 수 있고 심한 경우 엔진에 크래시가 발생할 수 있습니다. 그래서 유니티는 에셋을 사용할 때 엔진 내에서의 메시징을 개선하기 위해 특정 조건에서 특정 API의 호출을 제한하는 작업을 하기 시작했습니다. 수정하기 어려운 버그와 일관성이 떨어지는 결과를 미연에 방지하고, 체계적이지 않은 워크플로에 익숙해지지 않도록 하기 위해 이 작업을 진행했습니다.

API 보호

Unity 에디터와 함께 제공되는 API에 새로운 내부 속성을 만들어 추가했습니다. 이 속성은 에디터 코드를 컴파일할 때 C#과 C++ 사이의 네이티브 바인딩 레이어에 코드를 자동으로 생성합니다.

[PreventExecutionInState]

이를 설계한 방식은 다음과 같습니다.

  • 데코레이션된 함수를 호출하면 안 되는 속성에 상태를 제공합니다.
  • 보호에 심각도(예외 또는 오류 메시지)를 지정합니다.
  • 문제의 '해결 방법'에 대한 상세한 설명을 제공합니다.

예를 들어 AssetDatabase.CreateAsset은 AssetDatabase.bindings.cs 내에서 다음과 같이 데코레이션됩니다.

[PreventExecutionInState
(
AssetDatabasePreventExecution.kGatheringDependencies
FromSourceFile, PreventExecutionSeverity.PreventExecution_ManagedException, 
"Assets may not be created during gathering of import dependencies"
)]

임포트 대기열에서 에셋을 가져와야 하는 위치를 파악하도록 AssetDatabase에 동적 종속성만 제공해야 하는 경우 GatherDependenciesFromSourceFile 콜백 중에 AssetDatabase.CreateAsset을 호출하여 에셋을 생성할 수 없습니다.

이 속성을 배치하면 Unity 에디터와 함께 제공되는 코드 생성에 이 속성을 사용합니다.

Unity 에디터 소스 코드가 컴파일되면(C#과 C++ 모두) 모든 C# 함수를 정의하는 C++ 코드 레이어가 자동 생성됩니다. 이러한 함수는 .bindings.cs 파일 내에서 extern으로 표시됩니다. 생성된 C++ 파일은 자동 생성된 파일이라고 명확하게 표시됩니다.

API에 제어 기능을 넣기 위해 새로운 C# 속성을 고려하고 이를 C++ 측에서 실행할 수 있게 변환하도록 코드 자동 생성 툴을 확장했습니다. 따라서 API를 호출하면 안 될 때 호출한 경우, API를 어떻게 사용할 것인지에 관해 자세한 정보를 제공할 수 있는 레이어 역할을 합니다.

자동 생성 코드

C# 코드(AssetDatabase.bindings.cs)

[NativeThrows]
[PreventExecutionInState(
  AssetDatabasePreventExecution.kGatheringDependenciesFromSourceFile,
  PreventExecutionSeverity.PreventExecution_ManagedException,
  "Assets may not be created during gathering of import dependencies"
  )]
extern public static void CreateAsset([NotNull] Object asset, string path);

자동 생성 코드는 PreventExecutionInStateAttribute에 대해 다음을 수행하는 스탠드얼론 C# 툴로 작성됩니다.

  • UnityEditor.dll 어셈블리의 메서드가 PreventExecutionInStateAttribute로 데코레이션된 경우 다음과 같은 관련 정보를 추출합니다.
    • 현재 속성이 있는 클래스 이름
      • 즉, AssetDatabase
    • 속성으로 데코레이션된 메서드 이름
      • 즉, CreateAsset
    • 상수가 정의된 열거형 클래스 이름
      • 즉, AssetDatabasePreventExecution
    • 열거형의 이름과 값
      • 즉, kGatheringDependenciesFromSourceFile
        • 리플렉션(Reflection)을 사용해 이름을 추출합니다.
private string GetNameFromCustomAttribute(IBindingsPreventExecution curCheck)
{
    var name = curCheck.someObject.GetType().GetProperty("Type").GetValue(curCheck.someObject).ToString();

    //일반적으로 이름은 UnityEditor.AssetDatabasePreventExecution과 유사합니다.
    //그러나 클래스 내부에 UnityEditor.MyClass/MyEnum과 같은 형식의
    //열거형이 있을 수 있으며 여기에서 'MyEnum' 부분을 추출해야 합니다.
    var splitName = name.Split('.');
    var lastEntry = splitName[splitName.Length - 1];
    int start = lastEntry.LastIndexOf("/") + 1;
    var fixedName = lastEntry.Substring(start, lastEntry.Length - start);

    return fixedName;
}
  • kGatheringDependenciesFromSourceFile = 1 << 3
    • 값도 리플렉션(Reflection)을 사용해 추출합니다.
//값을 얻는 방법은 더 간단합니다. 값은 항상 정수일 것이며 더 구체적으로는 어떤 조건이 실패했는지
//보고할 수 있어야 하므로 값은 2의 제곱일 것입니다.
//따라서 조건별로 하나의 비트가 있어야 합니다.
var flag = curCheck.someObject.GetType().GetProperty("Value").GetValue(curCheck.someObject);
int flagValue = int.Parse(flag.ToString());
  • PreventExecutionSeverity 파라미터의 값
    • PreventExecution_ManagedException
      • 심각도를 나타내기 위해 만든 C++ 열거형 PreventExecutionSeverity를 추가합니다.
  • 보호 중인 시스템이 특정 상태에 있을 때 해당 API를 왜 호출하면 안 되는지 메시지로 설명합니다.
    • "Assets may not be created during gathering of import dependencies"("임포트 종속성을 수집하는 동안 에셋이 생성되지 않을 수 있습니다")

또한 함수의 윗부분에 여러 개의 PreventExecutionInStateAttribute 인스턴스가 있는 경우, 이러한 API를 호출할 때 성능 저하를 최소화하기 위해 해당 조건들을 병합하여 한 번에 확인합니다.

자동 생성 코드 가독성 높이기

필요한 모든 정보를 확보한 후에는 동등한 C++ 코드를 생성해야 합니다. 자동 생성된 C++ 코드의 각 부분은 C#에서의 원래 속성 정의와 연결됩니다. 즉, CreateAsset 메서드는 아래의 자동 생성된 C++ 코드에서 문자열 'CreateAsset'에 매핑됩니다.

마지막으로, 모든 정보를 추출하면 자동 생성된 코드가 생성되고 PreventExecutionInStateAttribute에서 사용하는 이름과 같은 변수와 네임스페이스, 클래스가 있게 됩니다.

자동 생성된 C++ 코드

if(g_AssetDatabasePreventExecutionBitField & AssetDatabasePreventExecutionChecks::kGatheringDependenciesFromSourceFile)
    AssetDatabasePreventExecution::ReportExecutionPrevention(
            "CreateAsset",
            AssetDatabasePreventExecutionChecks::kGatheringDependenciesFromSourceFile,
            PreventExecutionSeverity::PreventExecution_ManagedException,
            "Assets may not be created during gathering of import dependencies",
            &exception
            );

다음과 같은 결과가 나타납니다.

  • 에셋 데이터베이스(g_AssetDatabasePreventExecutionBitField)의 상태를 특정 조건과 비교하여 쿼리합니다. (이 경우 GatherDependenciesFromSourceFile 콜백 실행)
    • 비트 필드는 가능한 한 오버헤드가 적으면서도 엔진 코드까지 내려가기 전에 호출을 중단할지 감지할 수 있어야 합니다.
  • 조건이 참이면 이 호출을 허용하지 않는다고 보고하고 이유를 출력합니다.
  • 그런 다음 C++로 AssetDatabase::CreateAsset 코드에 대응하는 부분을 호출하기 전에 Mono를 통해 관리되는 예외를 발생시킵니다. 이렇게 해서 특정 에셋에 어떤 에셋 종속성이 있어야 하는지 Unity에 알리는 동안 에셋 생성으로 인해 원치 않는 부작용이 발생하지 않도록 합니다.
  • 조건이 거짓이면 일반 조건에서와 같이 네이티브 AssetDatabase::CreateAsset 코드를 실행합니다.

결과적으로 API의 대응되는 C++ 코드는 적절할 때 실행되며, 문제를 해결하는 데 도움이 되도록 관련 메시지를 담은 오류가 제공됩니다.

또한 g_AssetDatabasePreventExecutionBitField의 이름에 AssetDatabasePreventExecution이 포함되어 있는 것을 확인할 수 있습니다. Unity의 다른 시스템이 자체 API 보호를 정의하고, 원치 않는 API 사용을 사전에 막기 위해 해당 부분을 일부러 이름에 포함했습니다.

총정리

이 API의 동작에 대한 변경 사항을 확인할 수 있도록 AssetDatabase Test Suite에 여러 테스트를 추가했습니다. GatherDependenciesFromSourceFile 호출 내에서 CreateAsset을 호출할 때마다 관리되는 예외가 발생합니다.

Capture of AssetDatabase Test Suite view when showing a call to GatherDependenciesFromSourceFile.

다양한 시나리오로 코드를 테스트하였으며, API는 의도한 바에 따라 작동할 예정입니다.

변경 사항 적용

API 보호 변경 사항을 적용하는 것은 동작 변경이라고 볼 수 있습니다. 따라서 더 다듬어진 메시징 기능을 제공하기 위해 노력하고 있습니다. 그러나 지금 가장 큰 문제는 유니티 커뮤니티에 미치는 영향입니다. 다양한 에셋 스토어 패키지, 사용자 프로젝트, 라이브러리, API 예제에서 API를 사용하기 때문입니다. 따라서 변경 사항을 적용했을 때 내부적으로 얼마나 많은 테스트가 실패하는지 확인하고 해당 정보를 사용하여 API 보호 변경 사항이 얼마나 큰 영향을 미칠 것인지 측정할 예정입니다. 너무 많은 시스템에서 현재 동작을 사용한다면 실제로는 변경하지 않도록 고려할 수도 있습니다. 

다행히도 GatherDependenciesFromSourceFile은 비교적 최신 콜백이며, 이를 사용하는 ScriptedImporters도 그리 많지 않습니다. 그러나 AssetDatabase.FindAssets 호출이 OnPostProcessAllAssets 등에서 제한된다면 내부 테스트뿐 아니라 Unity 사용자에게도 많은 반발이 있을 거라 짐작됩니다. (이는 가정일 뿐이며 실제로는 제한할 계획이 없으므로 걱정하지 않아도 됩니다.) OnPostProcessAllAssets 내에서 AssetDatabase.FindAssets를 호출하는 것은 매우 일반적으로 사용되는 방법이기 때문입니다.

이번 포스팅의 내용에 관해 토론하고 싶다면 에셋 데이터베이스 포럼에서 다음 스레드로 이동하세요. Twitter에서 @jav_dev를 팔로우하고 연재 중인 Tech from the Trenches 시리즈를 통해 유니티 개발자의 여러 기술 블로그를 확인하세요.

2022년 9월 12일 테크놀로지 | 9 분 소요
관련 게시물