관리 메뉴

cyphen156

ResourceManager 리팩토링 중간 정리 본문

프로젝트/Skul 모작

ResourceManager 리팩토링 중간 정리

cyphen156 2026. 2. 4. 20:12

요약은 클로드가 햇시우

ResourceManager 버전 비교 및 통합 분석

개요

두 개의 ResourceManager.cs 파일이 존재하며, 각각 다른 리소스 관리 접근 방식을 사용하고 있습니다.


주요 차이점

1. 아키텍처 접근 방식

첫 번째 파일 (최신 버전)

  • TypeMapContainer 기반 시스템
  • AccessMode (Public/Internal) 구분
  • Task 기반 비동기 로딩
  • StreamContainer를 통한 파일 스트림 관리
  • Addressables 시스템 통합

두 번째 파일 (이전 버전)

  • ContentManifest 기반 시스템
  • Coroutine 기반 콘텐츠 동기화
  • 서버 검증 및 업데이트 시스템
  • 번들 다운로드 및 캐싱

기능 비교표

기능첫 번째 파일두 번째 파일
비동기 처리 Task/async-await Coroutine
파일 스트림 관리 StreamContainer + 리스 시스템 직접 File I/O
리소스 컨테이너 TypeMapContainer (형식 안전) Dictionary (기본)
서버 동기화 ❌ 없음 ✅ ContentManifest 검증
번들 관리 Addressables 기본 커스텀 번들 다운로드
메타데이터 관리 DomainAddressResolver ContentMeta + 서버 검증
에러 처리 IOResult 구조체 try-catch + 로그
동시성 제어 lock + Task 큐잉 Coroutine 순차 처리

각 버전의 강점

첫 번째 파일 강점

  1. 현대적인 C# 패턴
    • async/await 사용
    • 형식 안전한 제네릭 시스템
    • RAII 패턴 (StreamContainer)
  2. 강력한 동시성 관리
 
 
csharp
   // 중복 로딩 방지
   Task<IOResult> previousTask = null;
   lock (internalContainer.LockObj) {
       TryGetAsset_Internal<Task<IOResult>>(staticKey, AccessMode.Internal, out previousTask);
   }
  1. StreamContainer 리스 시스템
    • 파일 중복 열기 방지
    • 자동 리소스 정리
    • Task 완료 시 자동 Dispose
  2. 상세한 IOResult
 
 
csharp
   public struct IOResult {
       public IOFailReason failReason;
       public Exception exception;
       public long httpResponseCode;
   }

두 번째 파일 강점

  1. 콘텐츠 업데이트 시스템
    • 서버 메타 검증
    • SHA256 해시 비교
    • 점진적 업데이트
  2. 번들 캐싱
 
 
csharp
   // {persistent}/Bundles/{schema}/{bundleId}/{sha256}.bundle
   string bundleDir = Path.Combine(bundleRoot, entry.id);
  1. 버전 관리
    • RequiredOnBoot 플래그
    • 지연 로딩 지원
    • 로컬 폴백 메커니즘

통합 권장사항

1. 코어 시스템: 첫 번째 파일 기반

  • TypeMapContainer 유지
  • Task 기반 비동기 유지
  • StreamContainer 리스 시스템 유지

2. 추가할 기능: 두 번째 파일에서

 
 
csharp
// ContentManifest 시스템 통합
internal async Task<IOResult> SyncContentManifestAsync(CancellationToken ct = default)
{
    // C_SyncContentManifest를 async/await로 변환
}

// 메타 검증을 Task 기반으로
internal async Task<(IOResult result, VerifyResult verifyResult)> 
    VerifyContentMetaAsync(ContentMeta localMeta, string id, string schema, CancellationToken ct = default)
{
    // C_VerifyContentMeta를 async/await로 변환
}

3. 통합 아키텍처

 
 
ResourceManager
├── Legacy Resources (Resources.Load)
├── TypeMapContainer System (형식 안전 캐시)
├── StreamContainer System (파일 스트림 관리)
├── Addressables Integration (첫 번째 파일)
└── Content Update System (두 번째 파일, async 변환)
    ├── ContentManifest
    ├── ContentMeta Verification
    └── Bundle Download/Cache

마이그레이션 계획

Phase 1: 기본 통합

  1. 첫 번째 파일을 베이스로 사용
  2. ContentManifest 관련 클래스 추가
  3. DomainAddressResolver와 ContentManifest 통합

Phase 2: 비동기 변환

 
 
csharp
// Before (Coroutine)
public IEnumerator C_SyncContentManifest() { ... }

// After (Task)
public async Task<IOResult> SyncContentManifestAsync(CancellationToken ct = default)
{
    LoadContentMeta(out localMeta, contentManifest.id, contentManifest.schema);
    
    var verifyResult = await VerifyContentMetaAsync(
        localMeta, 
        contentManifest.id, 
        contentManifest.schema, 
        ct
    );
    
    if (verifyResult.result == VerifyResult.Outdated) {
        await UpdateContentManifestAsync(verifyResult, ct);
    }
}

Phase 3: 스트림 통합

 
 
csharp
// 번들 다운로드에 StreamContainer 사용
public async Task<IOResult> DownloadBundleAsync(
    string uri, 
    string bundlePath, 
    CancellationToken ct = default)
{
    // DownloadFileAsync 재사용
    return await DownloadFileAsync(uri, bundlePath, ct);
}

잠재적 문제점 및 해결책

문제 1: Coroutine vs Task 혼용

현상: Unity의 일부 API는 Coroutine만 지원 해결:

 
 
csharp
public async Task<IOResult> LoadAssetAsync<T>(uint staticKey) where T : UnityEngine.Object
{
    var tcs = new TaskCompletionSource<IOResult>();
    
    // Addressables는 Task 지원
    var handle = Addressables.LoadAssetAsync<T>(resolvedPath);
    await handle.Task;
    
    return IOResult.Ok();
}

문제 2: 이중 리소스 시스템

현상: Resources.Load와 Addressables 혼재 해결:

 
 
csharp
public bool TryGetAsset<T>(uint staticKey, out T asset) where T : UnityEngine.Object
{
    // 1. TypeMapContainer 확인 (Addressables)
    if (TryGetAsset_Internal<T>(staticKey, AccessMode.Public, out asset)) {
        return true;
    }
    
    // 2. Legacy Resources 폴백
    if (domainAddressResolver.TryResolve(staticKey, out string path)) {
        asset = Resources.Load<T>(path);
        return asset != null;
    }
    
    return false;
}

문제 3: 메모리 관리

현상: 여러 캐시 딕셔너리 존재 해결:

 
 
csharp
internal void ClearCache(CacheType type)
{
    switch (type) {
        case CacheType.Sprites:
            itemSprites.Clear();
            monsterSprites.Clear();
            worldObjectSprites.Clear();
            break;
        case CacheType.TypeMaps:
            foreach (var container in assetContainers) {
                container?.Clear();
            }
            break;
        case CacheType.All:
            // 전체 정리
            ForceGarbageCollecting();
            break;
    }
}

성능 최적화 제안

1. 스프라이트 캐싱 통합

 
 
csharp
// 현재: 3개의 별도 딕셔너리
private readonly Dictionary<uint, Sprite> itemSprites = new();
private readonly Dictionary<uint, Sprite> monsterSprites = new();
private readonly Dictionary<uint, Sprite> worldObjectSprites = new();

// 제안: TypeMapContainer 활용
AllocateTypeMap<Sprite>(AccessMode.Public);

public Sprite GetSprite(uint objectKey) {
    if (TryGetAsset<Sprite>(objectKey, out var sprite)) {
        return sprite;
    }
    // Resources.Load 폴백
    return LoadSpriteFromResources(objectKey);
}

2. 비동기 초기화

 
 
csharp
// 현재: Awake에서 동기 로딩
private void Awake() {
    Initialize(); // 모든 리소스 동기 로드
}

// 제안: 백그라운드 초기화
private void Awake() {
    InitializeCore(); // 필수 항목만
    _ = InitializeAsync(); // 나머지는 비동기
}

private async Task InitializeAsync() {
    await LoadUserDataAsync();
    await SyncContentManifestAsync();
}

3. 지연 로딩

 
 
csharp
public Sprite GetControlSprite(string controlName, bool isHighlight = false)
{
    // 현재: 모든 스프라이트 사전 로드
    // 제안: 첫 사용 시 로드
    
    if (!controlSpritesLoaded) {
        LoadControlSprites(currentControlScheme);
    }
    
    // 기존 로직...
}

테스트 시나리오

1. 동시성 테스트

 
 
csharp
[Test]
public async Task LoadAsset_Concurrent_ShouldNotDuplicate()
{
    var tasks = new List<Task<IOResult>>();
    for (int i = 0; i < 10; i++) {
        tasks.Add(rm.LoadAssetAsync<Sprite>(testKey));
    }
    
    var results = await Task.WhenAll(tasks);
    
    // 모두 성공해야 함
    Assert.IsTrue(results.All(r => r.succeeded));
    
    // 실제로는 한 번만 로드되어야 함
    // (내부 카운터나 로그로 검증)
}

2. 스트림 리스 테스트

 
 
csharp
[Test]
public void StreamLease_SamePath_ShouldDeny()
{
    var container1 = rm.TryGetStreamContainer(testPath, "Owner1", out var reason1);
    Assert.IsTrue(container1.Succeeded);
    
    var container2 = rm.TryGetStreamContainer(testPath, "Owner2", out var reason2);
    Assert.IsFalse(container2.Succeeded);
    Assert.IsTrue(reason2.Contains("already leased"));
    
    container1.Release();
    
    var container3 = rm.TryGetStreamContainer(testPath, "Owner3", out var reason3);
    Assert.IsTrue(container3.Succeeded);
}

3. 콘텐츠 업데이트 테스트

 
 
csharp
[Test]
public async Task SyncManifest_Outdated_ShouldUpdate()
{
    // Mock 서버 설정
    mockServer.SetupMeta("manifest", "v2", "hash_v2");
    
    var result = await rm.SyncContentManifestAsync();
    
    Assert.IsTrue(result.succeeded);
    Assert.AreEqual("v2", rm.contentManifest.version);
}

결론

권장 접근

  1. 첫 번째 파일을 기반으로 유지
  2. 두 번째 파일의 콘텐츠 업데이트 시스템을 async/await로 변환하여 통합
  3. 단계적 마이그레이션으로 안정성 확보

우선순위

  1. ✅ TypeMapContainer 시스템 유지 (완료)
  2. 🔄 ContentManifest를 Task 기반으로 변환 (진행 필요)
  3. 🔄 스프라이트 캐싱 통합 (진행 필요)
  4. ⏳ 비동기 초기화 구현 (대기)
  5. ⏳ 포괄적인 테스트 작성 (대기)

예상 효과

  • 성능: Task 기반 비동기로 메인 스레드 부담 감소
  • 안정성: StreamContainer로 파일 충돌 방지
  • 유지보수: 형식 안전 시스템으로 버그 감소
  • 확장성: ContentManifest로 동적 콘텐츠 업데이트 지원