앱인토스 개발자센터 로고
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 이내 메모리 사용
  • 토스 브랜딩 에셋: 토스 디자인 시스템 리소스 우선 로딩
  • 토스페이 연동: 결제 관련 리소스 사전 준비
  • 모바일 최적화: 기기 성능에 따른 동적 품질 조정
  • 분석 연동: 리소스 사용량 데이터 앱인토스 분석 시스템 전송

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