유니티 에셋 번들 빌드 시간 단축 사례

2024. 2. 1. 23:37Unity, C# 프로그래밍

조금 예전에 작업했던 일이지만..
잊어버리기 전에 블로그에 적어두면 좋을 것 같아서 한번 써보는 글.

 

개요

회사에서는 지금 Jenkins로 앱 빌드 및 에셋번들 (패치파일) 빌드를 하고 있다.

 

보통 간단한 에셋번들 관련 테스트는

매일 아침에 자동으로 빌드되는 결과물들로 잘 사용하지만

가끔씩 내 작업 브랜치에서 변경된 구조로 번들을 뽑아서 확인해봐야 할 때가 있다.

 

번들 빌드를 한번 새로 뽑을때마다 1시간 30분 정도 소요되었었는데

 

개발 단계 테스트 할 때마다, 또 이후 QA 테스트 요청 할 때

Windows, iOS, Android 번들을 다 뽑아서 테스트해야 하다보니 시간이 엄청 오래 걸렸고

 

이거 더 빠르게 할 수 없나?! 하면서 빌드 로직을 조금 손봤었는데

그 과정에 대한 기록이다.

 

문제점

그때 살펴본 결과 크게 2가지 문제가 있었는데

 

1. 오래됐고, 비효율적으로 보이는 API 사용.

2. async/await의 잘못된 사용.

 

이렇게 2가지가 있었다.

 

 

문제점 1. 오래됐고, 비효율적으로 보이는 API 사용

첫번째로 번들 빌드를 위해 호출하는 함수가 아래 함수였는데

public static bool BuildAssetBundle(Object mainAsset, Object[] assets, string pathName, out uint crc, BuildAssetBundleOptions assetBundleOptions, BuildTarget targetPlatform)

https://docs.unity3d.com/ScriptReference/BuildPipeline.BuildAssetBundle.html

 

일단 인자로 Object[] 를 받는다.

이렇게 되면 빌드를 위해 한 번들에 들어가야 하는 모든 에셋을 메모리에 역직렬화해서 올린 후에 API를 호출하게 되는데

이거.. 딱 봐도 느리고 메모리도 많이 사용할 것 같았다.

 

(심지어 Obsolete 표시가 되어있을 정도로 오래된 API)

 

해당 API를 대체하는 신규 API를 살펴보면

public static AssetBundleManifest BuildAssetBundles(string outputPath, AssetBundleBuild[] builds, BuildAssetBundleOptions assetBundleOptions, BuildTarget targetPlatform)

https://docs.unity3d.com/2020.3/Documentation/ScriptReference/BuildPipeline.BuildAssetBundles.html

 

AssetBundleBuild 구조체 배열을 받는데

해당 구조체는 번들의 이름과 그 번들에 들어갈 에셋의 경로 문자열을 배열로 가지고 있다.

 

기존에 사용했던 API는 에셋을 역직렬화해서 메모리에 다 올린 후에 호출할 수 있지만

이 함수는 그렇지 않기 때문에 기존 대비 메모리에서 이점을 가질 수 있을거라고 생각했다.

 

해당 함수를 적용하고 필요 변경점들은 아래에서 다시 서술한다. (아예 똑같은 결과물이 나오진 않는다)

 

문제점 2. async/await의 잘못된 사용

기존 로직도 사실 n년 전에 누군가가 동기 빌드를 비동기로 개선한 결과물이였다.

async/await를 이용한 함수들로 저 위의 BuileAssetBundle 함수를 병렬적으로 호출하려고 했던 흔적이 있었는데

 

결론적으로는 동기적으로 작동하고 있었다.

Task.Run이나 Task.Factory.StartNew를 이용하지 않고 async 함수를 호출하게 되면

유니티에서는 유니티가 재정의해놓은 UnitySynchronizationContext라는 동기화 컨텍스트에 후속 작업이 예약되게 된다.

 

그리고 해당 컨텍스트에 예약되게 되면 메인 스레드에서 폴링하는 방식으로 동작한다.

 

결국 모양만 async/await이고 동기식으로 동작하고 있었다.

(그렇다고 Task.Run 해서 유니티 API 를 호출하면.. 당연하게도 에러가 난다)

 

멀티 스레드를 이용한 작업을 할 때는 항상 잘 알아보고 작업해야 한다!

한 식당이 있는데
해당 식당에는 주방장이 한명 있다.

이때 손님 1명이 한번에 메뉴 10개를 시키는 것과
손님 10명이 각각 메뉴 1개씩 시키는 것

이렇게 두가지 경우가 있다고 하면
사실 후자는 주방장이 좀 더 어지러울 뿐이지 메뉴가 빨리 나오지는 않을 것이다.

손님 10명을 준비하기 전에
주방이 여러명의 요청을 받을 준비가 되어있는지 먼저 확인해보는게 좋다.

 

그래서 async/await 관련 코드를 모두 들어냈다.

빌드 이후 개발용 파일 서버에 업로드 하는 부분만 Task를 사용하도록 수정했다.

 

변경 후 변경 사항 및 문제점

일단 최신 API인 BuildAssetBundles는 확인해본 결과 내부적으로 멀티 스레딩을 사용해서 빌드한다.

코드에서 async/await를 모두 제거했지만? 오히려 변경한 쪽이 코어를 모두 활용한다.

 

변경 후 빌드 시간은 30분 정도로

거의 기존 대비 33% 수준이였다.

 

다만 빌드하는 API가 변경되었기 때문에 몇가지 문제점이 있었는데

 

첫번째로 API 호출할 때 번들 이름을 어떻게 넣던간에

결과 파일의 이름이 무조건 소문자로 나오는 문제가 있었는데

 

이건 미리 이름을 기억해 두었다가 빌드 후에 파일 이름을 바꿔주는 방식으로 해결!

(옵션이 있나 찾아봤는데 없었다.. ㅜ)

 

두번째로 셰이더 및 머터리얼이 각각 다른 번들에 들어가면 셰이더가 깨지는 문제가 있었다.

 

기존 API는 각각의 번들 빌드마다 참조하는 셰이더가 각 번들에 복사되어 들어가고 있었는데

신규 API는 전체 번들 중 셰이더 A가 포함된 번들이 하나라도 있다면 단일 인스턴스를 보장한다.

 

* 아래는 에셋번들과 종속성 관련 문서 발췌

에셋 번들에 속한 UnityEngine.Objects 중 하나 이상이 다른 번들에 있는 UnityEngine.Object에 대한 레퍼런스를 가지고 있을 경우 해당 에셋 번들은 다른 에셋 번들에 종속합니다.

하지만, UnityEngine.Object에 에셋 번들이 포함되지 않은 UnityEngine.Object에 대한 레퍼런스가 포함되어 있으면 종속 관계는 형성되지 않습니다. 이 경우 번들이 종속하는 오브젝트가 에셋 번들이 빌드되는 번들로 복사됩니다.

만약 다수의 번들에 존재하는 다수의 오브젝트가 특정 번들에 할당되지 않은 동일한 오브젝트에 대한 레퍼런스가 포함된 경우, 해당 오브젝트에 종속 관계를 가지는 모든 번들은 해당 오브젝트를 각각 복사하여 빌드된 에셋 번들에 포함합니다.

에셋 번들이 종속성을 포함하는 경우, 인스턴스화하는 오브젝트가 로딩되기 이전에 종속성을 가지는 번들이 로딩되도록 해야 합니다. Unity 엔진은 종속성을 자동으로 로딩하지 않습니다.

예제를 하나 들어보겠습니다.
번들 1 에 있는 머티리얼이 번들 2 에 있는 텍스처를 참조한다고 가정하겠습니다.
이 경우 번들 1 의 머티리얼을 로딩하기 이전, 번들 2 를 메모리로 로딩해야 합니다. 번들 1 과 번들 2 를 로딩하는 순서는 중요하지 않습니다. 다만, 번들 1 의 머티리얼이 로딩되기 이전에 번들 2 가 로딩되어야만 합니다. 다음 섹션에서는 이전 섹션에서 다룬 AssetBundleManifest 오브젝트를 활용하여 어떻게 런타임 시점에 종속성을 결정하고 로딩할 수 있는지 알아보겠습니다.

https://docs.unity3d.com/kr/2018.4/Manual/AssetBundles-Dependencies.html

 

그리고 이것과 관련해서 하나 더 문제가 있었는데

 

머티리얼 A와, 이 머티리얼 A에 연결된 셰이더 B가 있을 때

이 A, B가 각각 다른 번들에 할당되어 있다면

 

셰이더 B가 포함된 번들을 빌드할 때

머티리얼 A에서 사용중인 shader feature를 알 수 없기 때문에 문제가 생긴다.

multi compile로 사용하면 문제는 없다고 합니다. (미사용 variant에 대한 검사 없이 다 포함이라?)
다만 보통 shader feature로 쓰기 때문에..

 

* 아래는 해당 문제 관련 유니티 포럼

https://forum.unity.com/threads/asset-bundles-and-shaders.806331/

* shader feature 및 multi compile의 차이

https://docs.unity3d.com/kr/2020.3/Manual/SL-MultipleProgramVariants.html

 

이 문제를 찾아보면서 https://docs.unity3d.com/2020.3/Documentation/ScriptReference/ShaderVariantCollection.html 를 알아봤었는데..

 

내부 논의 결과 에셋 번들 빌드 시에 shader는 모두 제외하는 방식으로 문제를 해결했다.

 

이렇게 하게 되면 각 Material이 참조하는 shader 인스턴스를 번들마다 들고 있게 되는데

이게 결국 기존 방식이라 문제가 적을 것 같아서 채택됐다.

 

아무튼 두번째 문제도 해결하며 에셋번들 빌드 개선 작업은 성공적으로 들어갔고

빌드 담당하시는분이 야근이 많이 줄었다고 좋아해 주셨다.

 

사실 라이브 이슈가 하나 있었는데

해당 테스크를 적용하면서 전체 번들을 다시 다 빌드해서 라이브에 올려야 했는데
구두 전달이 잘못 되어서 기존(V1 빌드)로 몇개 뽑고 개선한 버전으로 몇개 뽑아서 섞어서 올라가는 바람에

특정 맵의 셰이더가 1주일간 마젠타 색으로 나왔다..ㅋㅋㅜㅜ.. 이슈 없는 줄 알았는데

 

정리하며

기존에 사용하고 있던 BuildAssetBundle..

 

이거 진짜 오래된 함수라서 사용 중인 회사가 있을지는 모르겠는데

혹시라도 있다면 이 글을 보고 편하게 API 변경 했으면 좋겠다!

 


 

블로그 글 봐주시는 분들도 혹시 궁금한 점 있다면 댓글로 물어보면 답변해드리겠습니다 ~