(25/06/25)리소스 메니저&LazyLoad,Closure

허윤·2025년 6월 25일

public class ResourceManager : SingleTon<ResourceManager>
{
    private SpriteAtlas sprites;
    public SpriteAtlas GetSprite 
    { 
        get
        {
            
            if (!isLoadAble<SpriteAtlas>(sprites))
            {
                Debug.LogError("ManagerException : ResourceManager의 sprite Instance가 비어있습니다. 호출 순서,Adressable key값 혹은 코드상의 키값을 확인해주세요");
                LoadAsync<SpriteAtlas>("spritePackage", (obj) => { sprites = obj; }, true);
            }
            return sprites;
        }
    }
    private bool isLoadAble<T>(T instance) { return instance != null; }
    //메니저 인스턴스 생성시 실행되는 함수
    public override void Init()
    {
        base.Init();
        if(!isLoadAble<SpriteAtlas>(sprites)) LoadAsync<SpriteAtlas>("spritePackage", (obj) => { sprites = obj; }, true);
    }
    /// <summary>
    /// 
    /// </summary>
    /// <typeparam name="T">리턴타입</typeparam>
    /// <param name="key">타겟 키값</param>
    /// <param name="callback">(obj)=>{targetInstance = obj}</param>
    public void LoadAsync<T>(string key, Action<T> callback, bool isCaching = false)
    {
        if (key.Contains(".sprite"))
        {
            key = $"{key}[{key.Replace(".sprite", "")}]";
        }
        AsyncOperationHandle<T> infoAsyncOP = Addressables.LoadAssetAsync<T>(key);
        infoAsyncOP.Completed += (op) =>
        {

            callback?.Invoke(infoAsyncOP.Result);
            if(!isCaching)Addressables.Release(infoAsyncOP);
        };
    }
    /// <summary>
    /// 
    /// </summary>
    /// <typeparam name="T">리턴타입</typeparam>
    /// <param name="label">타겟 키값</param>
    /// <param name="callback">(obj)=>{targetInstance = obj}</param>
    public void LoadAsyncAll<T>(string label, Action<(string, T)[]> callback, bool isCaching = false)
    {
        var labelKeys = Addressables.LoadResourceLocationsAsync(label, typeof(T));
        //label의 T타입인 오브젝트들의 키값을 가져온다
        labelKeys.WaitForCompletion();
        //resource를 전부 load할때까지 대기

        Debug.Log(labelKeys.Result);
        if (labelKeys.Result.Count == 0) { Debug.LogError($"{label}라벨이 비어있습니다."); callback.Invoke(null); }//해당하는 키가 없을경우 null을 리턴

        int doneCount = 0;

        (string, T)[] tempT = new (string, T)[labelKeys.Result.Count];
        for (int i = 0; i < tempT.Length; i++)
        {
            int curIndex = i;
            string curKey = labelKeys.Result[i].PrimaryKey; //콜백을 동시에 실행시 클로저 이슈가 생길 수 있기에 분리(다른 루프의 스택메모리를 참조하는 현상)
            LoadAsync<T>(labelKeys.Result[i].PrimaryKey, (result) =>
            {
                tempT[curIndex].Item1 = curKey;
                tempT[curIndex].Item2 = result;
                doneCount++;
                if (doneCount == labelKeys.Result.Count)
                {
                    callback?.Invoke(tempT);
                    if (!isCaching) Addressables.Release(labelKeys);
                }
            }, isCaching);
        }
    }
}

TIL

    1. 오늘 학습 키워드
    • LazyLoad,Closure
    1. 오늘 학습 한 내용을 나만의 언어로 정리하기
    • 에셋을 비동기로 캐싱할 경우 생길 수 있는 레이지 로드와 콜백 환경에서 생길 수 있는 클로저 이슈 학습
    1. 학습하며 겪었던 문제점 & 에러
    • 문제&에러에 대한 정의
      • ResourceManager Instance를 접근하여 캐싱된 데이터를 사용 할 때 캐싱이 안되어있는 자료들이 생길 수 있다.(LazyLoad)
      • Stack메모리인 int를 lamda식이 캡처하는 과정에서 참조형으로 바꾸며 모든 콜백이 바뀐 참조형을 사용하며 일어나는 문제
    • 내가 한 시도와 해결 방법
      • Await Task를 사용하면 lazyLoad를 해결 할 수 있을 것 같지만 그냥 인스턴스 생성시에 한번 캐싱하주는 형태로 진행
        유니티에서 Await을 썻다가 헬게이트가 열리기 쉽상이라..
      • 별도의 스택영역을 확보해주는 형태로 해결하였다.
    • 새롭게 알게 된 점
      • 클로저 문제 자체는 알고있었으나 원인은 잘 몰랐는데 대략적인 스코프와 일어나는 원리를 알게되었다.
    • 이 문제&에러를 다시 만나게 되었다면?
      • 표면적으로 보이는 코드와 다른 산출값이 나온다면 클로저 이슈를 의심해볼 수 있게 되었다 그때도 똑같이 새로운 스택영역 혹은 멀티쓰래드 환경이 가능하다면 lock을 사용해서 해결하는 방법이 있을 것 같다.
    1. 다음 학습 할 것은 무엇인지
    • 프로젝트를 진행하며 정리할 것 같다.
profile
C# 클라이언트 프로그래밍 지망

0개의 댓글