앱인토스 개발자센터 로고
Skip to content
이 내용이 도움이 되었나요?

리소스 로딩 최적화

앱인토스 Unity 게임에서 리소스를 효율적으로 로딩하고 관리하여 메모리 사용량을 최소화하고 성능을 극대화하는 방법을 제공해요.

1. 리소스 로딩 전략

앱인토스 리소스 관리 원칙

📦 앱인토스 리소스 관리 전략
├── 초기 번들 (Critical Bundle) - 5MB 이내
│   ├── 게임 엔진 코어
│   ├── 앱인토스 SDK
│   ├── 첫 화면 UI
│   └── 필수 폰트/아이콘
├── 기능별 번들 (Feature Bundles) - 각 10MB 이내
│   ├── 게임플레이 에셋
│   ├── UI 시스템
│   ├── 오디오 에셋
│   └── 이펙트 시스템
├── 토스 연동 번들 (Toss Integration) - 3MB 이내
│   ├── 토스페이 UI
│   ├── 토스 로그인 리소스
│   └── 분석 시스템
└── 온디맨드 번들 (On-Demand) - 유연한 크기
    ├── 레벨별 에셋
    ├── 캐릭터 스킨
    └── 계절 이벤트 리소스

리소스 분류 시스템

c#
using UnityEngine;
using UnityEngine.AddressableAssets;
using System.Collections.Generic;
using System.Linq;

[System.Serializable]
public class ResourceCategory
{
    public string categoryName;
    public ResourcePriority priority;
    public ResourceType resourceType;
    public List<string> assetLabels;
    public long maxCacheSizeMB = 50;
    public float cacheTimeoutMinutes = 30;
    public bool compressOnDownload = true;
    public bool enableStreaming = false;
}

public enum ResourcePriority
{
    Critical = 0,    // 즉시 필요한 리소스
    High = 1,        // 곧 사용될 리소스  
    Medium = 2,      // 백그라운드 로딩
    Low = 3,         // 필요시 로딩
    Toss = 4         // 앱인토스 특화 리소스
}

public enum ResourceType
{
    Texture,
    Audio,
    Model,
    Animation,
    UI,
    Script,
    Font,
    Shader,
    TossIntegration // 앱인토스 연동 전용
}

public class AppsInTossResourceManager : MonoBehaviour
{
    public static AppsInTossResourceManager Instance { get; private set; }
    
    [Header("리소스 카테고리")]
    public ResourceCategory[] categories;
    
    [Header("앱인토스 설정")]
    public long totalMemoryLimitMB = 200; // 앱인토스 메모리 제한
    public bool enableTossAnalytics = true;
    public bool optimizeForMobile = true;
    
    // 리소스 캐시 관리
    private Dictionary<string, CachedResource> resourceCache = new Dictionary<string, CachedResource>();
    private Dictionary<string, ResourceCategory> categoryMap = new Dictionary<string, ResourceCategory>();
    private Queue<string> loadingQueue = new Queue<string>();
    
    [System.Serializable]
    private class CachedResource
    {
        public object resource;
        public ResourceCategory category;
        public System.DateTime loadTime;
        public System.DateTime lastAccessTime;
        public int accessCount;
        public long memorySizeBytes;
        public UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle handle;
    }
    
    void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject);
            InitializeResourceManager();
        }
        else
        {
            Destroy(gameObject);
        }
    }
    
    void InitializeResourceManager()
    {
        Debug.Log("앱인토스 리소스 매니저 초기화");
        
        // 카테고리 맵 생성
        foreach (var category in categories)
        {
            categoryMap[category.categoryName] = category;
        }
        
        // 메모리 모니터링 시작
        InvokeRepeating(nameof(MonitorMemoryUsage), 10f, 10f);
        
        // 앱인토스 특화 리소스 사전 로딩
        StartCoroutine(PreloadTossResources());
    }
    
    // 리소스 로딩 API
    public void LoadResourceAsync<T>(string address, System.Action<T> onComplete, 
        System.Action<string> onError = null) where T : UnityEngine.Object
    {
        StartCoroutine(LoadResourceCoroutine<T>(address, onComplete, onError));
    }
    
    System.Collections.IEnumerator LoadResourceCoroutine<T>(string address, 
        System.Action<T> onComplete, System.Action<string> onError) where T : UnityEngine.Object
    {
        // 캐시 확인
        if (resourceCache.ContainsKey(address))
        {
            var cached = resourceCache[address];
            cached.lastAccessTime = System.DateTime.UtcNow;
            cached.accessCount++;
            
            onComplete?.Invoke(cached.resource as T);
            yield break;
        }
        
        Debug.Log($"리소스 로딩 시작: {address}");
        float startTime = Time.realtimeSinceStartup;
        
        // Addressable 에셋 로딩
        var handle = Addressables.LoadAssetAsync<T>(address);
        yield return handle;
        
        if (handle.Status == UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationStatus.Succeeded)
        {
            // 캐시에 추가
            var category = DetermineResourceCategory(address, typeof(T));
            var cached = new CachedResource
            {
                resource = handle.Result,
                category = category,
                loadTime = System.DateTime.UtcNow,
                lastAccessTime = System.DateTime.UtcNow,
                accessCount = 1,
                memorySizeBytes = EstimateResourceSize(handle.Result),
                handle = handle
            };
            
            resourceCache[address] = cached;
            
            float loadTime = Time.realtimeSinceStartup - startTime;
            Debug.Log($"리소스 로딩 완료: {address} ({loadTime:F2}초)");
            
            // 앱인토스 분석에 로딩 데이터 전송
            if (enableTossAnalytics)
            {
                SendResourceLoadingAnalytics(address, loadTime, cached.memorySizeBytes, true);
            }
            
            onComplete?.Invoke(handle.Result);
        }
        else
        {
            string error = $"리소스 로딩 실패: {address} - {handle.OperationException}";
            Debug.LogError(error);
            
            if (enableTossAnalytics)
            {
                SendResourceLoadingAnalytics(address, -1, 0, false);
            }
            
            onError?.Invoke(error);
        }
    }
    
    ResourceCategory DetermineResourceCategory(string address, System.Type resourceType)
    {
        // 주소와 타입 기반으로 카테고리 결정
        if (address.Contains("Toss") || address.Contains("AIT"))
        {
            return categoryMap.ContainsKey("TossIntegration") ? 
                categoryMap["TossIntegration"] : categories.FirstOrDefault();
        }
        
        foreach (var category in categories)
        {
            foreach (var label in category.assetLabels)
            {
                if (address.Contains(label))
                {
                    return category;
                }
            }
        }
        
        return categories.FirstOrDefault(); // 기본 카테고리
    }
    
    long EstimateResourceSize(UnityEngine.Object resource)
    {
        if (resource is Texture2D texture)
        {
            return texture.width * texture.height * 4; // RGBA32 추정
        }
        else if (resource is AudioClip audio)
        {
            return audio.samples * audio.channels * 2; // 16-bit PCM 추정
        }
        else if (resource is Mesh mesh)
        {
            return mesh.vertexCount * 32; // 추정값
        }
        else if (resource is GameObject go)
        {
            // GameObject의 컴포넌트들을 기반으로 추정
            return EstimateGameObjectSize(go);
        }
        
        return 1024 * 1024; // 1MB 기본값
    }
    
    long EstimateGameObjectSize(GameObject go)
    {
        long totalSize = 0;
        
        // 메시 렌더러
        var meshRenderers = go.GetComponentsInChildren<MeshRenderer>();
        foreach (var mr in meshRenderers)
        {
            var meshFilter = mr.GetComponent<MeshFilter>();
            if (meshFilter != null && meshFilter.mesh != null)
            {
                totalSize += meshFilter.mesh.vertexCount * 32;
            }
        }
        
        // 텍스처
        var renderers = go.GetComponentsInChildren<Renderer>();
        foreach (var renderer in renderers)
        {
            foreach (var material in renderer.materials)
            {
                if (material.mainTexture is Texture2D tex)
                {
                    totalSize += tex.width * tex.height * 4;
                }
            }
        }
        
        return totalSize;
    }
    
    // 배치 로딩
    public void LoadResourcesBatch<T>(List<string> addresses, 
        System.Action<Dictionary<string, T>> onComplete,
        System.Action<float> onProgress = null) where T : UnityEngine.Object
    {
        StartCoroutine(LoadResourcesBatchCoroutine<T>(addresses, onComplete, onProgress));
    }
    
    System.Collections.IEnumerator LoadResourcesBatchCoroutine<T>(List<string> addresses,
        System.Action<Dictionary<string, T>> onComplete,
        System.Action<float> onProgress) where T : UnityEngine.Object
    {
        var results = new Dictionary<string, T>();
        int loadedCount = 0;
        
        foreach (var address in addresses)
        {
            bool loadCompleted = false;
            T loadedResource = null;
            
            LoadResourceAsync<T>(address,
                (resource) => {
                    loadedResource = resource;
                    loadCompleted = true;
                },
                (error) => {
                    loadCompleted = true;
                }
            );
            
            yield return new WaitUntil(() => loadCompleted);
            
            if (loadedResource != null)
            {
                results[address] = loadedResource;
            }
            
            loadedCount++;
            float progress = (float)loadedCount / addresses.Count;
            onProgress?.Invoke(progress);
        }
        
        onComplete?.Invoke(results);
    }
    
    void MonitorMemoryUsage()
    {
        long totalMemoryBytes = 0;
        var expiredResources = new List<string>();
        
        foreach (var kvp in resourceCache)
        {
            var cached = kvp.Value;
            totalMemoryBytes += cached.memorySizeBytes;
            
            // 만료된 리소스 확인
            var timeSinceLastAccess = System.DateTime.UtcNow - cached.lastAccessTime;
            if (timeSinceLastAccess.TotalMinutes > cached.category.cacheTimeoutMinutes)
            {
                expiredResources.Add(kvp.Key);
            }
        }
        
        long totalMemoryMB = totalMemoryBytes / (1024 * 1024);
        
        // 메모리 제한 체크
        if (totalMemoryMB > totalMemoryLimitMB)
        {
            Debug.LogWarning($"리소스 메모리 사용량 초과: {totalMemoryMB}MB > {totalMemoryLimitMB}MB");
            OptimizeMemoryUsage();
        }
        
        // 만료된 리소스 정리
        foreach (var address in expiredResources)
        {
            UnloadResource(address);
        }
        
        if (enableTossAnalytics && totalMemoryMB > 0)
        {
            SendMemoryUsageAnalytics(totalMemoryMB, resourceCache.Count);
        }
    }
    
    void OptimizeMemoryUsage()
    {
        Debug.Log("리소스 메모리 최적화 시작");
        
        // 우선순위와 사용 빈도 기반으로 정렬
        var sortedResources = resourceCache.ToList();
        sortedResources.Sort((a, b) => {
            var scoreA = CalculateResourceScore(a.Value);
            var scoreB = CalculateResourceScore(b.Value);
            return scoreA.CompareTo(scoreB);
        });
        
        // 하위 30% 리소스 언로드
        int resourcesToUnload = Mathf.CeilToInt(sortedResources.Count * 0.3f);
        
        for (int i = 0; i < resourcesToUnload && i < sortedResources.Count; i++)
        {
            var address = sortedResources[i].Key;
            var cached = sortedResources[i].Value;
            
            // Critical 우선순위는 보호
            if (cached.category.priority != ResourcePriority.Critical)
            {
                UnloadResource(address);
            }
        }
        
        Debug.Log($"메모리 최적화 완료: {resourcesToUnload}개 리소스 언로드");
    }
    
    float CalculateResourceScore(CachedResource cached)
    {
        // 점수가 낮을수록 언로드 우선순위 높음
        float score = 0f;
        
        // 우선순위 점수 (낮을수록 중요)
        score += (int)cached.category.priority * 10f;
        
        // 사용 빈도 점수
        score += cached.accessCount * 5f;
        
        // 최근 사용 시간 점수
        var timeSinceAccess = (System.DateTime.UtcNow - cached.lastAccessTime).TotalMinutes;
        score -= (float)timeSinceAccess * 0.1f;
        
        // 메모리 크기 페널티
        var sizeMB = cached.memorySizeBytes / (1024f * 1024f);
        score -= sizeMB * 2f;
        
        return score;
    }
    
    public void UnloadResource(string address)
    {
        if (resourceCache.ContainsKey(address))
        {
            var cached = resourceCache[address];
            
            // Addressables 핸들 해제
            if (cached.handle.IsValid())
            {
                Addressables.Release(cached.handle);
            }
            
            resourceCache.Remove(address);
            Debug.Log($"리소스 언로드: {address}");
        }
    }
    
    // 앱인토스 특화 기능
    System.Collections.IEnumerator PreloadTossResources()
    {
        Debug.Log("앱인토스 특화 리소스 사전 로딩 시작");
        
        var tossResources = new List<string>
        {
            "TossPayIcon",
            "TossLogo", 
            "TossButton",
            "TossNotification",
            "TossFont",
            "TossColorPalette"
        };
        
        foreach (var resource in tossResources)
        {
            LoadResourceAsync<UnityEngine.Object>(resource, 
                (loaded) => {
                    Debug.Log($"앱인토스 리소스 로딩 완료: {resource}");
                },
                (error) => {
                    Debug.LogWarning($"앱인토스 리소스 로딩 실패: {resource} - {error}");
                }
            );
            
            yield return new WaitForSeconds(0.1f); // 부하 분산
        }
        
        Debug.Log("앱인토스 특화 리소스 사전 로딩 완료");
    }
    
    public void PreloadTossPayResources(System.Action onComplete = null)
    {
        StartCoroutine(PreloadTossPayResourcesCoroutine(onComplete));
    }
    
    System.Collections.IEnumerator PreloadTossPayResourcesCoroutine(System.Action onComplete)
    {
        var tossPayResources = new List<string>
        {
            "TossPayUI",
            "PaymentMethods", 
            "ReceiptTemplate",
            "PaymentSuccessEffect",
            "PaymentFailureEffect"
        };
        
        yield return StartCoroutine(LoadResourcesBatchCoroutine<UnityEngine.Object>(
            tossPayResources,
            (results) => {
                Debug.Log($"토스페이 리소스 로딩 완료: {results.Count}/{tossPayResources.Count}");
                onComplete?.Invoke();
            }
        ));
    }
    
    void SendResourceLoadingAnalytics(string address, float loadTime, long memorySize, bool success)
    {
        var analyticsData = new Dictionary<string, object>
        {
            {"resource_address", address},
            {"load_time", loadTime},
            {"memory_size_mb", memorySize / (1024f * 1024f)},
            {"success", success},
            {"timestamp", System.DateTime.UtcNow.ToString("o")}
        };
        
        AppsInToss.SendAnalytics("resource_loading", analyticsData);
    }
    
    void SendMemoryUsageAnalytics(long memoryUsageMB, int cachedResourceCount)
    {
        var analyticsData = new Dictionary<string, object>
        {
            {"memory_usage_mb", memoryUsageMB},
            {"cached_resources", cachedResourceCount},
            {"memory_limit_mb", totalMemoryLimitMB},
            {"timestamp", System.DateTime.UtcNow.ToString("o")}
        };
        
        AppsInToss.SendAnalytics("memory_usage", analyticsData);
    }
    
    // 공개 API
    public bool IsResourceCached(string address)
    {
        return resourceCache.ContainsKey(address);
    }
    
    public void ClearCache(ResourcePriority maxPriority = ResourcePriority.Low)
    {
        var addressesToRemove = new List<string>();
        
        foreach (var kvp in resourceCache)
        {
            if (kvp.Value.category.priority >= maxPriority)
            {
                addressesToRemove.Add(kvp.Key);
            }
        }
        
        foreach (var address in addressesToRemove)
        {
            UnloadResource(address);
        }
        
        Debug.Log($"캐시 정리 완료: {addressesToRemove.Count}개 리소스 제거");
    }
    
    public ResourceLoadingReport GenerateLoadingReport()
    {
        var report = new ResourceLoadingReport();
        
        report.totalCachedResources = resourceCache.Count;
        report.totalMemoryUsageMB = resourceCache.Values.Sum(r => r.memorySizeBytes) / (1024f * 1024f);
        report.memoryLimitMB = totalMemoryLimitMB;
        
        // 카테고리별 통계
        report.categoryStats = categories.Select(category => {
            var categoryResources = resourceCache.Values.Where(r => r.category == category);
            return new ResourceCategoryStats
            {
                categoryName = category.categoryName,
                resourceCount = categoryResources.Count(),
                memoryUsageMB = categoryResources.Sum(r => r.memorySizeBytes) / (1024f * 1024f),
                averageAccessCount = categoryResources.Any() ? 
                    categoryResources.Average(r => r.accessCount) : 0f
            };
        }).ToList();
        
        return report;
    }
}

[System.Serializable]
public class ResourceLoadingReport
{
    public int totalCachedResources;
    public float totalMemoryUsageMB;
    public float memoryLimitMB;
    public List<ResourceCategoryStats> categoryStats;
}

[System.Serializable] 
public class ResourceCategoryStats
{
    public string categoryName;
    public int resourceCount;
    public float memoryUsageMB;
    public float averageAccessCount;
}

2. 텍스처 최적화

모바일 최적화 텍스처 관리

c#
public class MobileTextureOptimizer : MonoBehaviour
{
    [System.Serializable]
    public class TextureQualitySettings
    {
        public string qualityLevel;
        public int maxTextureSize;
        public TextureFormat preferredFormat;
        public bool enableMipMaps;
        public FilterMode filterMode;
        public int compressionQuality;
    }
    
    [Header("기기별 텍스처 설정")]
    public TextureQualitySettings[] qualityTiers;
    
    [Header("앱인토스 최적화")]
    public bool enableDynamicQuality = true;
    public bool adaptToMemoryPressure = true;
    public long lowMemoryThresholdMB = 150;
    
    private TextureQualitySettings currentQuality;
    
    void Start()
    {
        DetermineOptimalQuality();
        
        if (adaptToMemoryPressure)
        {
            InvokeRepeating(nameof(CheckMemoryPressure), 30f, 30f);
        }
    }
    
    void DetermineOptimalQuality()
    {
        // 기기 성능에 따른 품질 결정
        int deviceTier = GetDevicePerformanceTier();
        
        if (deviceTier < qualityTiers.Length)
        {
            currentQuality = qualityTiers[deviceTier];
            ApplyTextureQuality(currentQuality);
        }
        
        Debug.Log($"텍스처 품질 설정: {currentQuality.qualityLevel}");
    }
    
    int GetDevicePerformanceTier()
    {
        // 메모리 크기 기반 기기 성능 분류
        int memoryGB = SystemInfo.systemMemorySize / 1024;
        
        if (memoryGB >= 8) return 0; // 고사양
        else if (memoryGB >= 4) return 1; // 중사양
        else if (memoryGB >= 2) return 2; // 저사양
        else return 3; // 초저사양
    }
    
    void ApplyTextureQuality(TextureQualitySettings quality)
    {
        QualitySettings.masterTextureLimit = GetTextureLimitFromSize(quality.maxTextureSize);
        QualitySettings.anisotropicFiltering = quality.filterMode == FilterMode.Trilinear ? 
            AnisotropicFiltering.Enable : AnisotropicFiltering.Disable;
        
        Debug.Log($"텍스처 품질 적용: 최대 크기 {quality.maxTextureSize}, 포맷 {quality.preferredFormat}");
    }
    
    int GetTextureLimitFromSize(int maxSize)
    {
        // Unity의 masterTextureLimit은 반대 방향 (0=원본, 1=1/2, 2=1/4)
        if (maxSize >= 2048) return 0;
        else if (maxSize >= 1024) return 1;
        else if (maxSize >= 512) return 2;
        else return 3;
    }
    
    void CheckMemoryPressure()
    {
        long currentMemoryMB = UnityEngine.Profiling.Profiler.GetTotalAllocatedMemory(false) / (1024 * 1024);
        
        if (currentMemoryMB > lowMemoryThresholdMB && enableDynamicQuality)
        {
            // 메모리 압박 시 텍스처 품질 하향 조정
            ReduceTextureQuality();
        }
    }
    
    void ReduceTextureQuality()
    {
        int currentTier = System.Array.IndexOf(qualityTiers, currentQuality);
        if (currentTier < qualityTiers.Length - 1)
        {
            currentQuality = qualityTiers[currentTier + 1];
            ApplyTextureQuality(currentQuality);
            
            Debug.LogWarning($"메모리 압박으로 텍스처 품질 하향: {currentQuality.qualityLevel}");
            
            // 앱인토스에 성능 이슈 리포트
            AppsInToss.ReportPerformanceIssue("texture_quality_reduced", currentQuality.qualityLevel);
        }
    }
    
    // 런타임 텍스처 압축
    public void CompressTexture(Texture2D texture, bool highQuality = false)
    {
        if (texture == null || !texture.isReadable) return;
        
        TextureFormat targetFormat = currentQuality.preferredFormat;
        
        // 앱인토스 환경에 최적화된 포맷 선택
        if (SystemInfo.SupportsTextureFormat(TextureFormat.ASTC_6x6))
        {
            targetFormat = highQuality ? TextureFormat.ASTC_4x4 : TextureFormat.ASTC_6x6;
        }
        else if (SystemInfo.SupportsTextureFormat(TextureFormat.ETC2_RGBA8))
        {
            targetFormat = TextureFormat.ETC2_RGBA8;
        }
        
        // 텍스처 압축 적용
        texture.Compress(highQuality);
        Debug.Log($"텍스처 압축 완료: {texture.name} -> {targetFormat}");
    }
}

3. 오디오 리소스 최적화

효율적인 오디오 관리

c#
public class AudioResourceManager : MonoBehaviour
{
    [System.Serializable]
    public class AudioClipData
    {
        public string clipName;
        public AudioType audioType;
        public AudioClipLoadType loadType;
        public bool compress;
        public float quality = 0.5f;
        public bool loop;
    }
    
    public enum AudioType
    {
        Music,
        SFX,
        Voice,
        UI,
        TossNotification // 앱인토스 특화
    }
    
    [Header("오디오 클립 설정")]
    public AudioClipData[] audioClips;
    
    [Header("앱인토스 오디오 설정")]
    public bool enableTossAudio = true;
    public bool optimizeForMobile = true;
    public int maxConcurrentAudioSources = 8;
    
    private Dictionary<string, AudioClip> loadedClips = new Dictionary<string, AudioClip>();
    private Dictionary<AudioType, float> typeVolumeSettings = new Dictionary<AudioType, float>();
    private Queue<AudioSource> audioSourcePool = new Queue<AudioSource>();
    
    void Start()
    {
        InitializeAudioSettings();
        CreateAudioSourcePool();
        
        if (enableTossAudio)
        {
            LoadTossAudioResources();
        }
    }
    
    void InitializeAudioSettings()
    {
        // 오디오 타입별 기본 볼륨 설정
        typeVolumeSettings[AudioType.Music] = 0.7f;
        typeVolumeSettings[AudioType.SFX] = 0.8f;
        typeVolumeSettings[AudioType.Voice] = 1.0f;
        typeVolumeSettings[AudioType.UI] = 0.6f;
        typeVolumeSettings[AudioType.TossNotification] = 0.9f; // 토스 알림음
        
        // 모바일 최적화 설정
        if (optimizeForMobile)
        {
            AudioSettings.GetConfiguration(out var config);
            config.numVirtualVoices = 256;
            config.numRealVoices = 32;
            AudioSettings.Reset(config);
        }
    }
    
    void CreateAudioSourcePool()
    {
        for (int i = 0; i < maxConcurrentAudioSources; i++)
        {
            var audioSourceGO = new GameObject($"AudioSource_{i}");
            audioSourceGO.transform.parent = transform;
            var audioSource = audioSourceGO.AddComponent<AudioSource>();
            audioSourcePool.Enqueue(audioSource);
        }
    }
    
    void LoadTossAudioResources()
    {
        var tossAudioResources = new List<string>
        {
            "TossPaySuccess",
            "TossPayFailure", 
            "TossNotification",
            "TossButtonClick",
            "TossSwipe"
        };
        
        foreach (var resource in tossAudioResources)
        {
            AppsInTossResourceManager.Instance.LoadResourceAsync<AudioClip>(resource,
                (clip) => {
                    loadedClips[resource] = clip;
                    Debug.Log($"앱인토스 오디오 리소스 로딩 완료: {resource}");
                },
                (error) => {
                    Debug.LogWarning($"앱인토스 오디오 리소스 로딩 실패: {resource}");
                }
            );
        }
    }
    
    public void PlayAudio(string clipName, AudioType audioType, float volume = -1f)
    {
        if (!loadedClips.ContainsKey(clipName))
        {
            // 동적 로딩
            AppsInTossResourceManager.Instance.LoadResourceAsync<AudioClip>(clipName,
                (clip) => {
                    loadedClips[clipName] = clip;
                    PlayLoadedAudio(clipName, audioType, volume);
                }
            );
            return;
        }
        
        PlayLoadedAudio(clipName, audioType, volume);
    }
    
    void PlayLoadedAudio(string clipName, AudioType audioType, float volume)
    {
        if (!loadedClips.ContainsKey(clipName)) return;
        
        var audioSource = GetAudioSource();
        if (audioSource == null)
        {
            Debug.LogWarning("사용 가능한 AudioSource가 없습니다");
            return;
        }
        
        audioSource.clip = loadedClips[clipName];
        audioSource.volume = volume >= 0 ? volume : typeVolumeSettings[audioType];
        audioSource.loop = GetAudioClipData(clipName)?.loop ?? false;
        
        audioSource.Play();
        
        // 재생 완료 후 AudioSource 반환
        StartCoroutine(ReturnAudioSourceAfterPlay(audioSource));
    }
    
    AudioSource GetAudioSource()
    {
        if (audioSourcePool.Count > 0)
        {
            return audioSourcePool.Dequeue();
        }
        
        // 풀이 비어있으면 새로 생성
        var audioSourceGO = new GameObject($"AudioSource_Dynamic");
        audioSourceGO.transform.parent = transform;
        return audioSourceGO.AddComponent<AudioSource>();
    }
    
    System.Collections.IEnumerator ReturnAudioSourceAfterPlay(AudioSource audioSource)
    {
        yield return new WaitUntil(() => !audioSource.isPlaying);
        
        audioSource.clip = null;
        audioSourcePool.Enqueue(audioSource);
    }
    
    AudioClipData GetAudioClipData(string clipName)
    {
        return System.Array.Find(audioClips, clip => clip.clipName == clipName);
    }
    
    // 앱인토스 특화 오디오 기능
    public void PlayTossNotification()
    {
        PlayAudio("TossNotification", AudioType.TossNotification);
        
        // 진동과 함께 재생 (앱인토스 네이티브 기능)
        AppsInToss.TriggerHapticFeedback(AppsInToss.HapticType.Notification);
    }
    
    public void PlayTossPaySound(bool success)
    {
        string clipName = success ? "TossPaySuccess" : "TossPayFailure";
        PlayAudio(clipName, AudioType.TossNotification);
        
        // 결제 결과에 따른 햅틱 피드백
        var hapticType = success ? AppsInToss.HapticType.Success : AppsInToss.HapticType.Error;
        AppsInToss.TriggerHapticFeedback(hapticType);
    }
    
    public void SetVolumeForType(AudioType audioType, float volume)
    {
        typeVolumeSettings[audioType] = Mathf.Clamp01(volume);
        
        // 현재 재생 중인 해당 타입의 오디오 볼륨 조정
        foreach (var audioSource in FindObjectsOfType<AudioSource>())
        {
            if (audioSource.clip != null)
            {
                var clipData = GetAudioClipData(audioSource.clip.name);
                if (clipData != null && clipData.audioType == audioType)
                {
                    audioSource.volume = volume;
                }
            }
        }
    }
    
    public void StopAllAudio(AudioType? specificType = null)
    {
        foreach (var audioSource in FindObjectsOfType<AudioSource>())
        {
            if (specificType.HasValue)
            {
                var clipData = GetAudioClipData(audioSource.clip?.name);
                if (clipData?.audioType == specificType.Value)
                {
                    audioSource.Stop();
                }
            }
            else
            {
                audioSource.Stop();
            }
        }
    }
    
    // 메모리 최적화
    public void UnloadUnusedAudioClips()
    {
        var clipsToRemove = new List<string>();
        
        foreach (var kvp in loadedClips)
        {
            bool isPlaying = false;
            
            foreach (var audioSource in FindObjectsOfType<AudioSource>())
            {
                if (audioSource.clip == kvp.Value && audioSource.isPlaying)
                {
                    isPlaying = true;
                    break;
                }
            }
            
            if (!isPlaying)
            {
                clipsToRemove.Add(kvp.Key);
            }
        }
        
        foreach (var clipName in clipsToRemove)
        {
            loadedClips.Remove(clipName);
        }
        
        Resources.UnloadUnusedAssets();
        Debug.Log($"사용하지 않는 오디오 클립 언로드: {clipsToRemove.Count}개");
    }
}

4. 체크리스트 및 권장사항

리소스 로딩 최적화 체크리스트

  • 리소스 카테고리 시스템 구현
  • 우선순위 기반 로딩 시스템 적용
  • 메모리 사용량 모니터링 시스템
  • 텍스처 최적화 시스템 적용
  • 오디오 리소스 관리 시스템
  • 동적 품질 조정 시스템
  • 캐시 관리 및 정리 시스템
  • 앱인토스 특화 리소스 사전 로딩
  • 성능 분석 및 리포팅 시스템
  • 메모리 압박 상황 대응 시스템

앱인토스 특화 권장사항

  • 메모리 제한 준수: 총 200MB 이내 메모리 사용
  • 토스 브랜딩 에셋: 토스 디자인 시스템 리소스 우선 로딩
  • 토스페이 연동: 결제 관련 리소스 사전 준비
  • 모바일 최적화: 기기 성능에 따른 동적 품질 조정
  • 분석 연동: 리소스 사용량 데이터 앱인토스 분석 시스템 전송

효율적인 리소스 관리는 성능과 사용자 경험의 핵심이에요. 앱인토스 환경에 최적화된 리소스 전략을 수립하세요.