μΉ΄ν…Œκ³ λ¦¬ μ—†μŒ

Unity 3D λŒ€κ·œλͺ¨ 씬 ν™˜κ²½μ—μ„œ Addressables 비동기 λ‘œλ”© μ‹œ λ°œμƒν•˜λŠ” GC 슀파이크 및 ν”„λ ˆμž„ μŠ€ν„°ν„°λ§ μ΅œμ ν™” κ°€μ΄λ“œ

κ²Œμž„κ΅μˆ˜ 2026. 8. 26. 07:01
λ°˜μ‘ν˜•

🌐 English Abstract

This article provides a deep technical analysis of Garbage Collection (GC) spikes and frame stuttering during asynchronous asset loading with Unity Addressables in large-scale 3D scenes. It covers the root causes within Unity's Managed Heap and Main Thread operation, profiling methodologies using Unity Profiler, and practical zero-allocation optimization techniques. Developers will learn actionable solutions including LZ4 compression tuning, custom UniTask wrapper implementation, and pre-allocated object pooling strategies for seamless performance.

μœ λ‹ˆν‹°(Unity 3D) μ—”μ§„μ—μ„œ λŒ€κ·œλͺ¨ 씬(Large-scale Scene)μ΄λ‚˜ μ˜€ν”ˆ μ›”λ“œ ν™˜κ²½μ„ κ°œλ°œν•  λ•Œ, 에셋 관리 및 λ©”λͺ¨λ¦¬ μ΅œμ ν™”λ₯Ό μœ„ν•΄ μ–΄λ“œλ ˆμŠ€λΈ” 에셋 μ‹œμŠ€ν…œ(Addressable Asset System)을 λ„μž…ν•˜λŠ” 것은 ν˜„μž¬ 업계 ν‘œμ€€μœΌλ‘œ 자리 μž‘μ•˜μŠ΅λ‹ˆλ‹€. κ·ΈλŸ¬λ‚˜ λ§Žμ€ κ°œλ°œμ§„μ΄ 비동기 λ‘œλ”© API인 Addressables.LoadAssetAsync λ˜λŠ” Addressables.InstantiateAsyncλ₯Ό μ±„νƒν–ˆμŒμ—λ„ λΆˆκ΅¬ν•˜κ³ , μ‹€μ‹œκ°„ κ²Œμž„ν”Œλ ˆμ΄ 도쀑 μ‹¬κ°ν•œ ν”„λ ˆμž„ μŠ€ν„°ν„°λ§(Frame Stuttering)κ³Ό GC 슀파이크(Garbage Collector Spikes) ν˜„μƒμ„ κ²ͺκ³€ ν•©λ‹ˆλ‹€.

λΆ„λͺ… '비동기(Asynchronous)'둜 λ‘œλ”©μ„ μ²˜λ¦¬ν•˜λ„λ‘ μ½”λ“œλ₯Ό μž‘μ„±ν–ˆμŒμ—λ„ λΆˆκ΅¬ν•˜κ³  μ™œ 메인 μŠ€λ ˆλ“œ(Main Thread)κ°€ λ©ˆμΆ”κ³  화면이 νŠ€λŠ” ν˜„μƒμ΄ λ°œμƒν• κΉŒμš”? λ³Έ ν¬μŠ€νŒ…μ—μ„œλŠ” 비동기 에셋 λ‘œλ”© λ™μž‘ κ³Όμ •μ—μ„œ μΌμ–΄λ‚˜λŠ” μœ λ‹ˆν‹° λ‚΄λΆ€ λ©”μ»€λ‹ˆμ¦˜μ„ 심도 있게 νŒŒν—€μΉ˜κ³ , ν”„λ‘œνŒŒμΌλ§ 진단법뢀터 ν˜„μ—…μ—μ„œ μ¦‰μ‹œ 적용 κ°€λŠ₯ν•œ μ‹€μ „ μ΅œμ ν™” κΈ°λ²•κΉŒμ§€ μ™„λ²½νžˆ λ‹€λ£Ήλ‹ˆλ‹€.



1. Addressables 비동기 λ‘œλ”© 쀑 GC μŠ€νŒŒμ΄ν¬κ°€ λ°œμƒν•˜λŠ” λ‚΄λΆ€ λ©”μ»€λ‹ˆμ¦˜

"비동기 λ‘œλ”©μ„ μ‚¬μš©ν•˜λ©΄ 메인 μŠ€λ ˆλ“œ μ—°μ‚°κ³Ό λ¬΄κ΄€ν•˜κ²Œ λ°±κ·ΈλΌμš΄λ“œ μŠ€λ ˆλ“œ(Background Thread)μ—μ„œ λͺ¨λ“  μ²˜λ¦¬κ°€ μ™„λ£Œλ  것이닀"λΌλŠ” 생각은 λŒ€ν‘œμ μΈ μ˜€ν•΄μž…λ‹ˆλ‹€. μœ λ‹ˆν‹°μ˜ C# κ΄€λ¦¬ν˜• νž™(Managed Heap) μ˜μ—­κ³Ό λ Œλ”λ§ νŒŒμ΄ν”„λΌμΈμ˜ 핡심 μž‘μ—…μ€ μ—¬μ „νžˆ 메인 μŠ€λ ˆλ“œμ—μ„œ 직렬화(Serialization)λ˜μ–΄ μ‹€ν–‰λ˜κΈ° λ•Œλ¬Έμž…λ‹ˆλ‹€.

κ°€. 비동기 μž‘μ—… 쀑 메인 μŠ€λ ˆλ“œ λΈ”λ‘œν‚Ή(Main Thread Blocking) 원인

μ–΄λ“œλ ˆμŠ€λΈ”μ΄ 에셋 묢음(AssetBundle)을 λ‘œλ“œν•  λ•Œ λ°±κ·ΈλΌμš΄λ“œ μŠ€λ ˆλ“œμ—μ„œλŠ” λ””μŠ€ν¬ I/O와 에셋 λ°μ΄ν„°μ˜ μ••μΆ• ν•΄μ œ(Decompression) μž‘μ—…μ΄ μΌμ–΄λ‚©λ‹ˆλ‹€. ν•˜μ§€λ§Œ μ••μΆ• ν•΄μ œλœ λ°”μ΄λ„ˆλ¦¬ 데이터λ₯Ό λ°”νƒ•μœΌλ‘œ μ‹€μ œ μœ λ‹ˆν‹° μ—”μ§„ λ‚΄λΆ€μ˜ C++ 에셋 객체(Mesh, Texture, Material λ“±)λ₯Ό μƒμ„±ν•˜κ³  메인 μŠ€λ ˆλ“œμ˜ λ Œλ”λ§ νŒŒμ΄ν”„λΌμΈμ— λ“±λ‘ν•˜λŠ” μž‘μ—…(Integration Phase)은 λ°˜λ“œμ‹œ 메인 μŠ€λ ˆλ“œμ—μ„œ μ΄λ£¨μ–΄μ§‘λ‹ˆλ‹€. 이 μž‘μ—…μ΄ 단일 ν”„λ ˆμž„μ— κ³Όλ„ν•˜κ²Œ λͺ°λ¦¬λ©΄ 메인 μŠ€λ ˆλ“œ 점유 μ‹œκ°„μ΄ κΈΈμ–΄μ Έ ν”„λ ˆμž„ λ“œλžμ΄ λ°œμƒν•©λ‹ˆλ‹€.

λ‚˜. κ΄€λ¦¬ν˜• λ©”λͺ¨λ¦¬ ν• λ‹Ή(Managed Memory Allocation)κ³Ό GC νƒ­

비동기 λ‘œλ”©μ„ ν˜ΈμΆœν•˜λŠ” κ³Όμ •μ—μ„œ λ°œμƒν•˜λŠ” Garbage(κ°€λΉ„μ§€)λŠ” 크게 μ„Έ κ°€μ§€ μš”μΈμ—μ„œ κΈ°μΈν•©λ‹ˆλ‹€:

  • C# Task 및 AsyncOperationHandle λ°•μ‹±(Boxing): AsyncOperationHandle을 μ œλ„€λ¦­ ꡬ쑰체 ν˜•νƒœλ‘œ λ‹€λ£¨κ±°λ‚˜ Task / Coroutine으둜 λ³€ν™˜(Awaiting)ν•˜λŠ” κ³Όμ •μ—μ„œ νž™ 할당이 λ°œμƒν•©λ‹ˆλ‹€.
  • λ¬Έμžμ—΄ 기반 ν‚€(String-based Address Keys) 검색: 에셋 μ£Όμ†Œκ°’μ„ string으둜 전달할 λ•Œ λ°œμƒν•˜λŠ” λŸ°νƒ€μž„ λ¬Έμžμ—΄ ν• λ‹Ή 및 λ”•μ…”λ„ˆλ¦¬(Dictionary) ν•΄μ‹œ 탐색 μ˜€λ²„ν—€λ“œμž…λ‹ˆλ‹€.
  • λ‚΄λΆ€ 콜백(Callback) λ§€κ°œλ³€μˆ˜ 캑처: 비동기 μž‘μ—… μ™„λ£Œ μ‹œ μ‹€ν–‰ν•  λžŒλ‹€μ‹(Lambda Expression)μ΄λ‚˜ 이벀트 ν•Έλ“€λŸ¬κ°€ μ™ΈλΆ€ λ³€μˆ˜λ₯Ό μΊ‘μ²˜ν•  λ•Œ 읡λͺ… 클래슀 객체가 νž™μ— μƒμ„±λ©λ‹ˆλ‹€.

μ΄λŸ¬ν•œ λ―Έμ„Έν•œ λ©”λͺ¨λ¦¬ 할당듀이 λŒ€κ·œλͺ¨ 씬 μ „ν™˜μ΄λ‚˜ λŒ€λŸ‰μ˜ 에셋(예: ν”„λ¦¬νŒΉ 50개 λ™μ‹œ λ‘œλ”©) 처리 μ‹œ μˆœκ°„μ μœΌλ‘œ 수 λ©”κ°€λ°”μ΄νŠΈ(MB) λ‹¨μœ„λ‘œ λˆ„μ λ˜λ©°, κ²°κ΅­ μœ λ‹ˆν‹°μ˜ 점진적 κ°€λΉ„μ§€ 컬렉터(Incremental GC)λ§ˆμ € μ œμ–΄ν•  수 μ—†λŠ” κ±°λŒ€ν•œ GC 슀파이크λ₯Ό μ΄‰λ°œν•˜κ²Œ λ©λ‹ˆλ‹€.

2. Profilerλ₯Ό ν™œμš©ν•œ 병λͺ© 지점 진단 및 μΈ‘μ •

문제 해결을 μœ„ν•΄μ„œλŠ” μ •ν™•ν•œ μΈ‘μ •(Measurement)이 μ„ ν–‰λ˜μ–΄μ•Ό ν•©λ‹ˆλ‹€. μœ λ‹ˆν‹° ν”„λ‘œνŒŒμΌλŸ¬(Unity Profiler)와 에셋 ν”„λ‘œνŒŒμΌλ§ 도ꡬλ₯Ό μ‚¬μš©ν•˜μ—¬ μŠ€ν„°ν„°λ§μ˜ 주범을 μΆ”μ ν•˜λŠ” λ°©λ²•μž…λ‹ˆλ‹€.

κ°€. Unity Profiler 핡심 μƒ˜ν”ŒλŸ¬ Marker 확인

ν”„λ‘œνŒŒμΌλŸ¬μ˜ CPU Usage λͺ¨λ“ˆμ—μ„œ Hierarchy λ·°λ₯Ό GC Alloc κΈ°μ€€μœΌλ‘œ μ •λ ¬ν•œ λ’€ λ‹€μŒ 마컀(Marker)듀을 좔적해야 ν•©λ‹ˆλ‹€:

  • AssetBundleRequest.GetResult / AwakeFromLoad: 에셋 직렬화 데이터λ₯Ό ν•΄μ œν•˜κ³  메인 μŠ€λ ˆλ“œμ— 객체λ₯Ό 생성할 λ•Œ λ°œμƒν•˜λŠ” 병λͺ©μž…λ‹ˆλ‹€. 이 μˆ˜μΉ˜κ°€ λ†’λ‹€λ©΄ 에셋 λ‹¨μœ„ λΆ„κ΄„(Granularity)이 잘λͺ»λ˜μ—ˆκ±°λ‚˜ 단일 ν”„λ ˆμž„μ— λ„ˆλ¬΄ λ§Žμ€ 에셋을 λ‘œλ“œν•˜κ³  μžˆμŒμ„ λœ»ν•©λ‹ˆλ‹€.
  • GC.Collect: λ©”λͺ¨λ¦¬ ν• λ‹Ή ν•œκ³„λ₯Ό μ΄ˆκ³Όν•˜μ—¬ κ°€λΉ„μ§€ 컬렉터가 λ™κΈ°μ μœΌλ‘œ μ‹€ν–‰λ˜μ—ˆμŒμ„ μ•Œλ¦¬λŠ” μ‹ ν˜Έμž…λ‹ˆλ‹€.
  • UpdateAllProviders: Addressables λ¦¬μ†ŒμŠ€ 관리 μ‹œμŠ€ν…œ λ‚΄λΆ€μ—μ„œ 릴리슀(Release) μƒνƒœ 및 쒅속성(Dependency)을 κ°±μ‹ ν•  λ•Œ λ°œμƒν•˜λŠ” νž™ ν• λ‹Ή μ§€μ μž…λ‹ˆλ‹€.

λ‚˜. Profile Analyzerλ₯Ό μ΄μš©ν•œ ν”„λ ˆμž„ κ°„ 격차 비ꡐ

단일 ν”„λ ˆμž„ λΆ„μ„λ§ŒμœΌλ‘œ λΆ€μ‘±ν•  경우, μœ λ‹ˆν‹° νŒ¨ν‚€μ§€ λ§€λ‹ˆμ €μ˜ Profile Analyzerλ₯Ό ν™œμš©ν•΄ λ‘œλ”© μ „/ν›„ 300ν”„λ ˆμž„ κ°„μ˜ Mean Time(평균 μ‹œκ°„)κ³Ό Max Time(μ΅œλŒ€ μ‹œκ°„)을 λΉ„κ΅ν•©λ‹ˆλ‹€. λ‘œλ”© 쀑 ν”„λ ˆμž„ νƒ€μž„μ΄ 16.6ms(60fps κΈ°μ€€)λ₯Ό λ„˜μ–΄μ„œλŠ” κ΅¬κ°„μ˜ GC Alloc λ°œμƒ νšŸμˆ˜μ™€ νž™ λ©”λͺ¨λ¦¬ 급증 지점을 μ‹œκ°μ μœΌλ‘œ 검증할 수 μžˆμŠ΅λ‹ˆλ‹€.



3. GC 슀파이크 및 μŠ€ν„°ν„°λ§ 해결을 μœ„ν•œ 4단계 μ΅œμ ν™” μ†”λ£¨μ…˜

단계 1: AssetBundle μ••μΆ• μ˜΅μ…˜ 및 λ‘œλ”© 버퍼 μ‘°μ • (LZ4 vs LZMA)

μ–΄λ“œλ ˆμŠ€λΈ” λΉŒλ“œ μ„€μ •(Addressable Asset Group Settings)μ—μ„œ κΈ°λ³Έ μ••μΆ• 방식을 μ΅œμ ν™”ν•΄μ•Ό ν•©λ‹ˆλ‹€. 기본값인 LZMA 압좕은 μ••μΆ•λ₯ μ΄ λ†’μ§€λ§Œ λŸ°νƒ€μž„μ— 전체 λ¬ΆμŒμ„ 메인 μŠ€λ ˆλ“œ λ©”λͺ¨λ¦¬μ— ν’€μ–΄μ•Ό ν•˜λ―€λ‘œ ν”„λ ˆμž„ λ“œλžμ˜ 주원인이 λ©λ‹ˆλ‹€.

  • LZ4 μ••μΆ• 적용: 청크 기반(Chunk-based) μ••μΆ• 방식인 LZ4λ₯Ό μ μš©ν•˜λ©΄ 에셋을 ν•„μš”ν•œ λΆ€λΆ„λ§Œ μ‹€μ‹œκ°„μœΌλ‘œ μ••μΆ• ν•΄μ œν•˜λ―€λ‘œ λ‘œλ”© 슀파이크λ₯Ό 획기적으둜 쀄일 수 μžˆμŠ΅λ‹ˆλ‹€.
  • AssetBundle λ‘œλ”© 버퍼(Loading Buffer) μ΅œμ ν™”: QualitySettings.asyncUploadBufferSizeλ₯Ό κΈ°λ³Έκ°’(4MB λ˜λŠ” 16MB)μ—μ„œ 64MB μ΄μƒμœΌλ‘œ 크게 λŠ˜λ €μ€λ‹ˆλ‹€. 이 λ²„νΌλŠ” ν…μŠ€μ²˜μ™€ λ©”μ‹œ 데이터λ₯Ό CPU λ©”λͺ¨λ¦¬λ₯Ό κ±°μΉ˜μ§€ μ•Šκ³  λ””μŠ€ν¬μ—μ„œ GPU VRAM으둜 직접 이관(Async Upload Pipeline)ν•˜λŠ” ν†΅λ‘œ 역할을 μˆ˜ν–‰ν•˜μ—¬ 메인 μŠ€λ ˆλ“œ 병λͺ©μ„ μ œκ±°ν•©λ‹ˆλ‹€.

단계 2: GC-Free C# Async Architecture (UniTask λ„μž…)

μœ λ‹ˆν‹° κΈ°λ³Έ Taskλ‚˜ Coroutine(YieldInstruction)은 비동기 호좜 μ‹œλ§ˆλ‹€ νž™ 객체λ₯Ό ν• λ‹Ήν•©λ‹ˆλ‹€. μ„±λŠ₯ μ΅œμ ν™”λ₯Ό μœ„ν•΄ UniTask 라이브러리λ₯Ό λ„μž…ν•˜κ³  AsyncOperationHandle의 직접적인 Task λ³€ν™˜μ„ μ§€μ–‘ν•΄μ•Ό ν•©λ‹ˆλ‹€.

단계 3: ν”„λ ˆμž„ λ²„μ§“νŒ…(Frame Budgeting) 기반 비동기 큐 κ΅¬ν˜„

λŒ€κ·œλͺ¨ μ”¬μ˜ ν”„λ¦¬νŒΉ 100개λ₯Ό λ™μ‹œμ— Addressables.InstantiateAsync둜 ν˜ΈμΆœν•˜λ©΄ 비동기 μž‘μ—…μ΄λΌ ν•˜λ”λΌλ„ 동일 ν”„λ ˆμž„μ— 메인 μŠ€λ ˆλ“œ μΈμŠ€ν„΄μŠ€ν™” μž‘μ—…μ΄ 쏠리게 λ©λ‹ˆλ‹€. 이λ₯Ό λ°©μ§€ν•˜κΈ° μœ„ν•΄ ν”„λ ˆμž„λ‹Ή μ‹œκ°„ μ˜ˆμ‚°(예: ν”„λ ˆμž„λ‹Ή μ΅œλŒ€ 3ms)을 μ„€μ •ν•˜κ³  μž‘μ—…μ„ λΆ„μ‚°μ‹œν‚€λŠ” μž‘μ—… 큐(Task Queue)λ₯Ό μ μš©ν•΄μ•Ό ν•©λ‹ˆλ‹€.

단계 4: Pre-Warmingκ³Ό 객체 풀링(Object Pooling)의 고도화 κ²°ν•©

μ–΄λ“œλ ˆμŠ€λΈ”μ„ 톡해 에셋을 λ‘œλ“œ(LoadAssetAsync)ν•œ ν›„ InstantiateAsyncλ₯Ό 직접 맀번 ν˜ΈμΆœν•˜λŠ” λŒ€μ‹ , 씬 μ§„μž… μ „ Pre-load λ‹¨κ³„μ—μ„œ μ—μ…‹μ˜ 원본 ν•Έλ“€λ§Œ λ©”λͺ¨λ¦¬μ— λ„μ›Œλ‘κ³ (Pre-warming), λŸ°νƒ€μž„ 생성을 μœ λ‹ˆν‹°μ˜ UnityEngine.Pool 기반 였브젝트 ν’€κ³Ό μ—°λ™ν•˜λŠ” νŒ¨ν„΄μœΌλ‘œ μ „ν™˜ν•΄μ•Ό ν•©λ‹ˆλ‹€.

4. μ‹€λ¬΄μš© κ³ μ„±λŠ₯ 비동기 에셋 λ‘œλ” μ½”λ“œ 예제

μ•„λž˜ 예제 μ½”λ“œλŠ” C#의 νž™ 할당을 κ·Ήλ‹¨μ μœΌλ‘œ μ€„μ΄λ©΄μ„œ(Zero-Allocation μ§€ν–₯), UniTask와 ν”„λ ˆμž„ λ²„μ§“νŒ… 방식을 μ‘°ν•©ν•˜μ—¬ μ–΄λ“œλ ˆμŠ€λΈ” 에셋을 μ•ˆμ „ν•˜κ²Œ 비동기 λ‘œλ”©ν•˜λŠ” μ‹€λ¬΄ν˜• 래퍼(Wrapper) κ΅¬ν˜„μ²΄μž…λ‹ˆλ‹€.

using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

public class ZeroAllocAddressableLoader : MonoBehaviour
{
    // 에셋 ν•Έλ“€ 캐싱을 ν†΅ν•œ μž¬λ‘œλ”© μ‹œ GC 및 I/O λ°œμƒ λ°©μ§€
    private readonly Dictionary<string, AsyncOperationHandle> _handleCache 
        = new Dictionary<string, AsyncOperationHandle>(128);

    /// <summary>
    /// GC Alloc을 μ΅œμ†Œν™”ν•œ μ•ˆμ „ν•œ 비동기 에셋 λ‘œλ”©
    /// </summary>
    public async UniTask<T> LoadAssetZeroAlloc<T>(string addressKey) where T : UnityEngine.Object
    {
        // 1. 이미 μΊμ‹œλœ 핸듀이 μ‘΄μž¬ν•˜λŠ”μ§€ 확인 (λ©”λͺ¨λ¦¬ λ‚­λΉ„ 및 쀑볡 λ‘œλ“œ λ°©μ§€)
        if (_handleCache.TryGetValue(addressKey, out var cachedHandle))
        {
            if (cachedHandle.IsValid())
            {
                return (T)cachedHandle.Result;
            }
            _handleCache.Remove(addressKey);
        }

        // 2. 비동기 λ‘œλ“œ μ‹œμž‘ (AssetReference λŒ€μ‹  λ‚΄λΆ€ λ¬Έμžμ—΄ ν‚€ 직접 이용)
        AsyncOperationHandle<T> handle = Addressables.LoadAssetAsync<T>(addressKey);

        // 3. UniTaskλ₯Ό ν™œμš©ν•˜μ—¬ νž™ ν• λ‹Ή(Task 객체 생성) 없이 await 처리
        await handle.ToUniTask(PlayerLoopTiming.Update);

        if (handle.Status == AsyncOperationStatus.Succeeded)
        {
            _handleCache[addressKey] = handle;
            return handle.Result;
        }

        Debug.LogError($"[AddressableLoader] 에셋 λ‘œλ“œ μ‹€νŒ¨: {addressKey}");
        if (handle.IsValid())
        {
            Addressables.Release(handle);
        }
        return null;
    }

    /// <summary>
    /// ν”„λ ˆμž„ μŠ€ν„°ν„°λ§ λ°©μ§€λ₯Ό μœ„ν•œ 큐 기반 λΆ„μ‚° μΈμŠ€ν„΄μŠ€ν™” μ˜ˆμ‹œ
    /// </summary>
    public async UniTask BatchInstantiateWithBudget(List<string> keys, float maxFrameTimeMs = 3.0f)
    {
        float frameBudgetSec = maxFrameTimeMs / 1000.0f;

        for (int i = 0; i < keys.Count; i++)
        {
            float startTime = Time.realtimeSinceStartup;

            // 에셋 λ‘œλ“œ
            GameObject prefab = await LoadAssetZeroAlloc<GameObject>(keys[i]);
            if (prefab != null)
            {
                Instantiate(prefab);
            }

            // ν˜„μž¬ ν”„λ ˆμž„ μ†Œμš” μ‹œκ°„μ΄ μ„€μ •λœ μ˜ˆμ‚°(Frame Budget)을 μ΄ˆκ³Όν•˜λ©΄ λ‹€μŒ ν”„λ ˆμž„μœΌλ‘œ λŒ€κΈ° 이관
            if (Time.realtimeSinceStartup - startTime > frameBudgetSec)
            {
                await UniTask.Yield(PlayerLoopTiming.Update);
            }
        }
    }

    private void OnDestroy()
    {
        // ν•Έλ“€ ν•΄μ œ λ©”λͺ¨λ¦¬ λˆ„μˆ˜ λ°©μ§€
        foreach (var handle in _handleCache.Values)
        {
            if (handle.IsValid())
            {
                Addressables.Release(handle);
            }
        }
        _handleCache.Clear();
    }
}


5. λŒ€κ·œλͺ¨ μ‹¬λ¦¬μŠ€ λ§΅ 적용 μ‹œ μ—£μ§€ μΌ€μ΄μŠ€(Edge Cases) λŒ€μ‘

κ°€. 쀑볡 에셋 λ²ˆλ“€(Duplicate Bundles)κ³Ό λ¬΄λΆ„λ³„ν•œ λ©”λͺ¨λ¦¬ 팽창

μ„œλ‘œ λ‹€λ₯Έ μ–΄λ“œλ ˆμŠ€λΈ” 그룹에 λ™μΌν•œ 머티리얼(Material)μ΄λ‚˜ ν…μŠ€μ²˜ 에셋이 μ°Έμ‘°λ˜μ–΄ μžˆλŠ” 경우, μœ λ‹ˆν‹°λŠ” 에셋 λ²ˆλ“€μ„ λΉŒλ“œν•  λ•Œ ν•΄λ‹Ή 에셋을 각 λ²ˆλ“€μ— μ€‘λ³΅μœΌλ‘œ ν¬ν•¨μ‹œμΌœ λ²„λ¦½λ‹ˆλ‹€. 이둜 인해 λ©”λͺ¨λ¦¬κ°€ μ΄μ€‘μœΌλ‘œ λ‚­λΉ„λ˜κ³  λŸ°νƒ€μž„ λ‘œλ”© μ‹œκ°„μ΄ λŠ˜μ–΄λ‚˜κ²Œ λ©λ‹ˆλ‹€.

ν•΄κ²°μ±…: Addressables Window의 Analyze Tool을 λ°˜λ“œμ‹œ 주기적으둜 μ‹€ν–‰ν•˜μ—¬ "Check Duplicate Bundle Dependencies" κ·œμΉ™μ„ 검사해야 ν•©λ‹ˆλ‹€. κ³΅ν†΅μœΌλ‘œ μ°Έμ‘°λ˜λŠ” 에셋듀은 λ³„λ„μ˜ Shared_Assets 그룹으둜 μΆ”μΆœν•˜μ—¬ 단일 λ²ˆλ“€λ‘œ 묢이도둝 쒅속성을 격리해야 ν•©λ‹ˆλ‹€.

λ‚˜. 저사양 λͺ¨λ°”일 κΈ°κΈ°μ—μ„œμ˜ Incremental GC ν•œκ³„

μœ λ‹ˆν‹°μ˜ 점진적 κ°€λΉ„μ§€ 컬렉터(Incremental GC)λŠ” GC μ‹€ν–‰ μ‹œκ°„μ„ μ—¬λŸ¬ ν”„λ ˆμž„μœΌλ‘œ λΆ„μ‚°μ‹œμΌœ μ£Όμ§€λ§Œ, μˆœκ°„μ μΈ νž™ ν• λ‹Ή 크기가 ν•œ ν”„λ ˆμž„μ˜ Incremental GC ν—ˆμš© λ²”μœ„λ₯Ό λ„˜μ–΄μ„œλ©΄ κ°•μ œ 동기(Synchronous) GC둜 μ „ν™˜λ©λ‹ˆλ‹€. 특히 저사양 λͺ¨λ°”일 κΈ°κΈ°(Android RAM 4GB μ΄ν•˜ λ“±)μ—μ„œλŠ” 이 μŠ€νŒŒμ΄ν¬κ°€ 50ms~100ms μ΄μƒμ˜ 멈좀 ν˜„μƒμœΌλ‘œ μ§κ²°λ©λ‹ˆλ‹€.

λ”°λΌμ„œ λŒ€κ·œλͺ¨ 씬 이동 μ „ System.GC.Collect() 및 Resources.UnloadUnusedAssets()λ₯Ό λ‘œλ”© ν™”λ©΄(Loading Screen)κ³Ό 같은 μ•ˆμ „ν•œ μ—°μΆœ κ΅¬κ°„μ—μ„œ λͺ…μ‹œμ μœΌλ‘œ μ„ μ œ ν˜ΈμΆœν•˜μ—¬, κ²Œμž„ν”Œλ ˆμ΄ 도쀑 μžλ™ GCκ°€ ν„°μ§€μ§€ μ•Šλ„λ‘ μ—¬μœ  νž™ λ©”λͺ¨λ¦¬λ₯Ό ν™•λ³΄ν•˜λŠ” μ „λž΅μ΄ ν•„μš”ν•©λ‹ˆλ‹€.



6. κ²°λ‘  및 μš”μ•½

Addressables μ‹œμŠ€ν…œμ„ ν™œμš©ν•œ 비동기 에셋 λ‘œλ”©μ€ λŒ€κ·œλͺ¨ μœ λ‹ˆν‹° ν”„λ‘œμ νŠΈμ˜ ν•„μˆ˜μš”μ†Œμ΄μ§€λ§Œ, λ‹¨μˆœνžˆ async/await ν‚€μ›Œλ“œλ₯Ό μ‚¬μš©ν•˜λŠ” κ²ƒλ§ŒμœΌλ‘œλŠ” ν”„λ ˆμž„ μŠ€ν„°ν„°λ§κ³Ό GC μŠ€νŒŒμ΄ν¬λ‘œλΆ€ν„° 자유둜울 수 μ—†μŠ΅λ‹ˆλ‹€.

  1. 에셋 μ••μΆ• 및 전솑 버퍼 μ΅œμ ν™”: LZ4 μ••μΆ• 방식을 μ‚¬μš©ν•˜κ³  asyncUploadBufferSizeλ₯Ό μ΅œμ†Œ 64MB μ΄μƒμœΌλ‘œ ν™•λ³΄ν•˜μ—¬ GPU 이관 병λͺ©μ„ μ€„μž…λ‹ˆλ‹€.
  2. C# λ©”λͺ¨λ¦¬ ν• λ‹Ή μ΅œμ†Œν™”: UniTask와 struct 기반 핸듀을 ν™œμš©ν•˜κ³  λžŒλ‹€μ‹ λ§€κ°œλ³€μˆ˜ 캑처 및 λ¬Έμžμ—΄ ν‚€ 할당을 λ°©μ§€ν•©λ‹ˆλ‹€.
  3. ν”„λ ˆμž„ μ˜ˆμ‚°(Frame Budgeting) λΆ„μ‚°: Instantiate 연산이 단일 ν”„λ ˆμž„μ— κ³Όλ„ν•˜κ²Œ 쏠리지 μ•Šλ„λ‘ 큐(Queue) μ‹œμŠ€ν…œμœΌλ‘œ μ΄κ΄€ν•©λ‹ˆλ‹€.
  4. 쀑볡 쒅속성 지속 뢄석: Analyze Tool을 ν™œμš©ν•΄ 묢음 κ°„ 쀑볡 λ¦¬μ†ŒμŠ€λ₯Ό μ œκ±°ν•˜κ³  사전 λ‘œλ”©(Pre-warming) 기법을 κ΅¬μ‘°ν™”ν•©λ‹ˆλ‹€.

μ΄λŸ¬ν•œ μ΅œμ ν™” μ•„ν‚€ν…μ²˜λ₯Ό ν”„λ‘œμ νŠΈ μ΄ˆκΈ°μ— 정립해 λ‘”λ‹€λ©΄, λŒ€κ·œλͺ¨ μ‹¬λ¦¬μŠ€ 3D ν™˜κ²½μ—μ„œλ„ 60 FPSλ₯Ό μ•ˆμ •μ μœΌλ‘œ μœ μ§€ν•˜λŠ” κ³ μ„±λŠ₯ λͺ¨λ°”일 및 PC κ²Œμž„μ„ μ„±κ³΅μ μœΌλ‘œ ꡬ좕할 수 μžˆμ„ κ²ƒμž…λ‹ˆλ‹€.

λ°˜μ‘ν˜•