Unity Addressables 카탈로그 원격 업데이트와 에셋 번들 CRC 무결성 검증 가이드

Unity Addressables 원격 카탈로그 업데이트 파이프라인 구축 방법과 에셋 번들 다운로드 시 CRC 무결성 검증, 캐시 손상 방지 및 복구 전략을 상세히 다룹니다.

✓ 복사 완료!
챕터일반 가이드
난이도중급
상태● 검토 완료
대상 엔진
Unity
최종 업데이트
Unity Addressables 카탈로그 원격 업데이트와 에셋 번들 CRC 무결성 검증 가이드

핵심 요약

  • 카탈로그 갱신 파이프라인: Addressables.InitializeAsync() → CheckForCatalogUpdates()로 원격 .hash 비교 → UpdateCatalogs()로 신규 .json 적용 → DownloadDependenciesAsync()로 번들 패치 순서로 진행됩니다.
  • CRC 오류 검출: Addressables 그룹의 CRC 설정으로 번들 다운로드 또는 캐시 로드 시 체크섬을 확인할 수 있습니다. CRC는 우발적인 손상을 검출하는 용도이며, 악의적인 변조를 막는 보안 검증 수단은 아닙니다.
  • 복구 전략: 다운로드 실패 원인을 확인하고 필요한 경우 Addressables.ClearDependencyCacheAsync(key)로 해당 키의 의존성 캐시를 지운 뒤 제한된 재시도를 수행합니다. 재시도만으로 모든 로딩 오류나 크래시를 방지할 수 있는 것은 아닙니다.

라이브 서비스를 진행하는 모바일 및 크로스 플랫폼 게임에서 클라이언트 빌드(APK, IPA 등)를 다시 배포하지 않고 리소스를 갱신하는 패치 파이프라인은 필수적입니다. Unity의 Addressable Asset System(어드레서블)은 에셋 번들의 빌드와 배포, 런타임 종속성 관리를 추상화해 주지만 실제 원격 패치 시스템을 구축할 때는 원격 카탈로그의 정확한 갱신 시점 제어와 네트워크 패킷 손상에 대응하는 CRC(Cyclic Redundancy Check) 무결성 검증을 세밀하게 설계해야 합니다.

네트워크 순단이나 CDN 캐시 불일치로 인해 손상된 에셋 번들이 디바이스 스토리지에 캐싱되면 이후 해당 에셋을 로드할 때 원인을 알기 힘든 NullReferenceException이나 메모리 크래시가 발생하게 됩니다. 본 글에서는 원격 카탈로그 검사부터 번들 다운로드, CRC 검증 및 복구까지의 완성도 높은 프로덕션 레벨 구현 가이드를 정리합니다.

Unity Addressables 원격 카탈로그와 번들 패치 파이프라인 개념도


Addressables 원격 카탈로그 시스템의 동작 원리

Addressables 시스템에서 카탈로그(Catalog)는 에셋의 주소(Address), GUID, 내부 키(InternalId) 그리고 해당 에셋이 포함된 실제 에셋 번들의 원격 URL과 종속성(Dependencies) 정보를 담고 있는 메타데이터 파일입니다.

원격 카탈로그 갱신을 활성화하면 빌드 시 두 가지 핵심 파일이 생성됩니다:

  1. {Id}.json: 에셋 주소와 번들 매핑 정보가 담긴 카탈로그 본문
  2. {Id}.hash: 카탈로그 본문 파일의 128비트 해시 문자열이 담긴 경량 텍스트 파일

클라이언트는 매번 수 메가바이트(MB)에 달하는 카탈로그 JSON을 통째로 다운로드하지 않고 먼저 수십 바이트 크기의 .hash 파일만 내려받아 로컬 캐시에 저장된 카탈로그 해시와 비교합니다. 해시값이 달라졌을 때만 신규 .json 카탈로그를 받아 메모리에 등록합니다.

sequenceDiagram
    autonumber
    participant Client as Game Client
    participant CDN as Remote CDN / Server
    participant Cache as Local Storage (Cache)

    Client->>CDN: 1. 원격 카탈로그 .hash 요청
    CDN-->>Client: 2. 최신 Hash 문자열 반환
    Client->>Client: 3. 로컬 저장 해시와 원격 해시 비교
    alt 해시 불일치 (새 버전 존재)
        Client->>CDN: 4. 최신 카탈로그 .json 요청
        CDN-->>Client: 5. 신규 카탈로그 메타데이터 반환
        Client->>Client: 6. 메모리 내 리소스 프로바이더 갱신
        Client->>CDN: 7. GetDownloadSizeAsync 확인 후 번들 다운로드 요청
        CDN-->>Client: 8. 에셋 번들 청크 전송
        Client->>Client: 9. CRC32 체크섬 계산 및 검증
        alt CRC 일치
            Client->>Cache: 10. 로컬 캐시 스토리지에 번들 영구 저장
        else CRC 불일치 (Data Corrupted)
            Client->>Cache: 11. 손상된 임시 청크 폐기
            Client->>Client: 12. 캐시 정리 및 재시도 로직 트리거
        end
    else 해시 일치 (최신 상태)
        Client->>Client: 13. 기존 로컬 번들 캐시 그대로 사용
    end

Addressables 카탈로그 업데이트는 어떤 순서로 구현할까?

안정적인 런타임 패치를 위해 AddressableAssetSettings에서 Disable Catalog Update On Startup 옵션을 활성화하고 클라이언트 스크립트에서 명시적인 단계(Step-by-Step)로 제어하는 것이 권장됩니다.

패치 매니저 단계별 구현 절차

  1. 초기화 (Addressables.InitializeAsync): Addressables 런타임 셋업을 완료합니다.
  2. 카탈로그 변경 확인 (Addressables.CheckForCatalogUpdates): 원격 .hash를 조회해 업데이트가 필요한 카탈로그 ID 목록을 반환받습니다.
  3. 카탈로그 갱신 (Addressables.UpdateCatalogs): 신규 카탈로그 메타데이터를 다운로드하여 내부 리소스 로케이터(Resource Locator)를 교체합니다.
  4. 다운로드 용량 산출 (Addressables.GetDownloadSizeAsync): 갱신된 카탈로그를 기준으로 실제로 내려받아야 할 번들의 총 바이트 수를 확인합니다.
  5. 에셋 번들 다운로드 (Addressables.DownloadDependenciesAsync): 신규/수정된 번들을 다운로드하고 로컬 디바이스에 캐싱합니다.

다음은 UI 진행률(Progress) 표시와 비동기 처리가 결합된 프로덕션용 C# 패치 매니저 구현 코드입니다.

using System;
using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks; // UniTask 사용 (표준 Task로 대체 가능)
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.AddressableAssets.ResourceLocators;
using UnityEngine.ResourceManagement.AsyncOperations;

public class RemotePatchManager : MonoBehaviour
{
    [SerializeField] private bool autoDownloadOnUpdate = true;

    public async UniTask<bool> RunPatchPipelineAsync(
        IProgress<float> progress,
        CancellationToken ct = default)
    {
        try
        {
            // Step 1: Addressables 초기화
            Debug.Log("[Patch] Addressables 초기화 시작");
            await Addressables.InitializeAsync().ToUniTask(cancellationToken: ct);

            // Step 2: 원격 카탈로그 변경 여부 확인
            Debug.Log("[Patch] 원격 카탈로그 변경 검사 중...");
            AsyncOperationHandle<List<string>> checkHandle = Addressables.CheckForCatalogUpdates(false);
            List<string> catalogsToUpdate = await checkHandle.ToUniTask(cancellationToken: ct);
            
            bool hasNewCatalog = catalogsToUpdate != null && catalogsToUpdate.Count > 0;
            Addressables.Release(checkHandle);

            if (hasNewCatalog)
            {
                Debug.Log($"[Patch] {catalogsToUpdate.Count}개의 신규 카탈로그 감지됨. 카탈로그 업데이트 수행");
                
                // Step 3: 카탈로그 업데이트 및 로케이터 갱신
                AsyncOperationHandle<List<IResourceLocator>> updateHandle = 
                    Addressables.UpdateCatalogs(catalogsToUpdate, false);
                await updateHandle.ToUniTask(cancellationToken: ct);
                Addressables.Release(updateHandle);
            }
            else
            {
                Debug.Log("[Patch] 카탈로그가 이미 최신 상태입니다.");
            }

            // Step 4: 다운로드 대상 용량 확인 (전체 프리로드 대상 키 사용, 예: "default" 또는 라벨)
            const string downloadKey = "Preload";
            AsyncOperationHandle<long> sizeHandle = Addressables.GetDownloadSizeAsync(downloadKey);
            long downloadSizeBytes = await sizeHandle.ToUniTask(cancellationToken: ct);
            Addressables.Release(sizeHandle);

            Debug.Log($"[Patch] 다운로드 필요 용량: {downloadSizeBytes / (1024f * 1024f):F2} MB");

            if (downloadSizeBytes <= 0)
            {
                Debug.Log("[Patch] 다운로드할 신규 번들이 없습니다. 패치 완료");
                progress?.Report(1.0f);
                return true;
            }

            if (!autoDownloadOnUpdate)
            {
                // 사용자에게 패치 용량 동의 팝업 띄우는 지점
                return false;
            }

            // Step 5: 번들 다운로드 및 무결성 검증 파이프라인 수행
            return await DownloadBundlesWithProgressAsync(downloadKey, progress, ct);
        }
        catch (OperationCanceledException)
        {
            Debug.LogWarning("[Patch] 패치 작업이 사용자에 의해 취소되었습니다.");
            return false;
        }
        catch (Exception ex)
        {
            Debug.LogError($"[Patch] 패치 파이프라인 오류 발생: {ex.Message}");
            return false;
        }
    }

    private async UniTask<bool> DownloadBundlesWithProgressAsync(
        object downloadKey,
        IProgress<float> progress,
        CancellationToken ct)
    {
        AsyncOperationHandle downloadHandle = Addressables.DownloadDependenciesAsync(downloadKey, false);

        while (!downloadHandle.IsDone)
        {
            ct.ThrowIfCancellationRequested();
            DownloadStatus status = downloadHandle.GetDownloadStatus();
            
            if (status.TotalBytes > 0)
            {
                float percentage = (float)status.DownloadedBytes / status.TotalBytes;
                progress?.Report(percentage);
            }

            await UniTask.Yield(PlayerLoopTiming.Update, ct);
        }

        bool isSuccess = downloadHandle.Status == AsyncOperationStatus.Succeeded;
        if (!isSuccess)
        {
            Debug.LogError($"[Patch] 번들 다운로드 실패: {downloadHandle.OperationException}");
        }
        else
        {
            Debug.Log("[Patch] 모든 에셋 번들 다운로드 및 캐싱 완료");
            progress?.Report(1.0f);
        }

        Addressables.Release(downloadHandle);
        return isSuccess;
    }
}

에셋 번들 CRC 무결성 검증은 왜 필수적이며 어떻게 동작할까?

CRC(Cyclic Redundancy Check) 검증 메커니즘

CRC는 데이터 전송이나 저장 과정에서 발생할 수 있는 에러를 검출하기 위해 사용하는 순환 중복 검사 알고리즘입니다. Unity 에셋 번들 시스템은 CRC-32-IEEE 802.3 다항식을 기반으로 32비트 정수 체크섬을 생성합니다.

수학적으로 바이트 시퀀스 M(x)M(x)를 생성 다항식 G(x)G(x)로 나누었을 때의 나머지 R(x)R(x)가 CRC 값이 됩니다:

M(x)⋅x32≡R(x)(modG(x))M(x) \cdot x^{32} \equiv R(x) \pmod{G(x)}

Addressables는 그룹의 Asset Bundle CRC 설정에 따라 번들을 로드하기 전 무결성 검사를 수행합니다. 이 검사는 손상 여부를 찾는 체크섬 검사이며, 서명 검증이나 인증을 제공하지 않습니다. 공격자가 번들과 기대 CRC 메타데이터를 함께 바꿀 수 있는 위협 모델이라면 CRC만으로 변조를 탐지할 수 없습니다.

Addressables 그룹의 CRC 검사 옵션 비교

Addressables 그룹의 Asset Bundle CRC 설정에는 Disabled, Enabled, Excluding Cached, Enabled, Including Cached 옵션이 있습니다. 관련 스키마 속성은 UseAssetBundleCrc와 UseAssetBundleCrcForCachedBundles입니다.

Asset Bundle CRC 옵션동작 방식고려 사항
Disabled번들 CRC 검사를 하지 않음손상 검출이 필요한 원격 번들에는 적합하지 않을 수 있음
Enabled, Excluding Cached다운로드하는 번들의 CRC를 검사하고 캐시에 있는 번들은 검사에서 제외Unity 문서가 원격 번들에 권장하는 기본 선택
Enabled, Including Cached캐시된 번들을 포함해 로드 전에 CRC를 검사캐시 파일도 반복 확인해야 하는 경우에 고려. 번들 검사 비용을 측정할 것

[!IMPORTANT]
CRC는 오류 검출용 체크섬이므로 변조 방지가 필요한 콘텐츠의 보안 경계로 사용하면 안 됩니다. 캐시 번들 검사 여부는 Addressables 버전과 프로젝트의 성능 요구를 확인한 뒤 프로파일링해 선택하십시오.

Addressables Group 인스펙터 내 Asset Bundle CRC 설정 화면


다운로드 실패 및 CRC 불일치 발생 시 복구 전략

CDN에서 이전 버전 번들이 아직 퍼징(Purge)되지 않았거나 중간 프록시 캐시 오염 등으로 인해 CRC Mismatch가 발생하면 번들 로드가 실패합니다. 이때 무작정 DownloadDependenciesAsync만 다시 호출하면 캐시 계층에 잘못된 정보가 남아 있어 연속 실패할 수 있습니다.

정밀 캐시 무효화 및 지수 백오프 재시도 구현

특정 에셋의 다운로드가 실패했을 때는 전체 캐시를 날리는 극단적인 방법 대신, Unity의 Caching API와 Addressables의 ClearDependencyCacheAsync를 사용하여 문제 번들만 정밀하게 무효화해야 합니다.

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

public static class ResilientAssetDownloader
{
    public static async UniTask<bool> DownloadWithRetryAsync(
        object key,
        int maxRetries = 3,
        int initialDelayMs = 1000,
        CancellationToken ct = default)
    {
        int currentAttempt = 0;
        int delay = initialDelayMs;

        while (currentAttempt < maxRetries)
        {
            currentAttempt++;
            Debug.Log($"[Download] 번들 다운로드 시도 ({currentAttempt}/{maxRetries}): {key}");

            AsyncOperationHandle handle = Addressables.DownloadDependenciesAsync(key, false);
            await handle.ToUniTask(cancellationToken: ct);

            if (handle.Status == AsyncOperationStatus.Succeeded)
            {
                Addressables.Release(handle);
                Debug.Log($"[Download] 번들 다운로드 성공: {key}");
                return true;
            }

            // 실패 원인 로깅
            Exception ex = handle.OperationException;
            Debug.LogWarning($"[Download] 다운로드 실패 (시도 {currentAttempt}): {ex?.Message}");
            Addressables.Release(handle);

            // CRC Mismatch 또는 캐시 오염 가능성에 대비해 해당 키의 의존성 캐시 제거
            Debug.LogWarning($"[Download] 손상 가능성이 있는 로컬 캐시 파기 중: {key}");
            AsyncOperationHandle<bool> clearHandle = Addressables.ClearDependencyCacheAsync(key, false);
            await clearHandle.ToUniTask(cancellationToken: ct);
            Addressables.Release(clearHandle);

            if (currentAttempt >= maxRetries)
            {
                break;
            }

            // 지수 백오프 (Exponential Backoff + Jitter)
            int jitter = UnityEngine.Random.Range(0, 300);
            int totalDelay = delay + jitter;
            Debug.Log($"[Download] {totalDelay}ms 후 재시도합니다.");
            await UniTask.Delay(totalDelay, cancellationToken: ct);

            delay *= 2; // 대기 시간 2배 증가
        }

        Debug.LogError($"[Download] 최대 재시도 횟수 초과로 최종 다운로드 실패: {key}");
        return false;
    }
}

자주 묻는 질문 (FAQ)

Q1. Disable Catalog Update On Startup 옵션을 반드시 켜야 하는 이유는 무엇인가요?

기본 설정 상태에서는 첫 씬이 로드될 때 Addressables 시스템이 동기/암시적으로 원격 카탈로그 확인을 시도합니다. 이 경우 네트워크가 연결되지 않았거나 CDN 응답이 느릴 때 게임 시작 화면에서 무한 대기(ANR)나 예외가 발생할 수 있습니다. 해당 옵션을 켜고 개발자가 직접 타이밍과 타임아웃, 예외 처리, 프로그레스 바 UI를 제어하는 것이 라이브 안정성 측면에서 안전합니다.

Q2. CRC Mismatch 발생 시 게임 전체 캐시(Caching.ClearCache())를 날려야 하나요?

전체 캐시 삭제는 권장되지 않습니다. 이미 정상 다운로드되어 검증이 끝난 다른 대용량 번들까지 모두 삭제되어 사용자가 수 기가바이트의 데이터를 다시 다운로드해야 하기 때문입니다. 위 예제처럼 Addressables.ClearDependencyCacheAsync(key)를 사용하여 실패한 키의 의존성 번들만 선택적으로 무효화해야 네트워크 비용과 사용자 이탈을 최소화할 수 있습니다.

Q3. 원격 서버(CDN)에 카탈로그와 번들을 올릴 때 배포 순서는 어떻게 해야 하나요?

반드시 새로운 에셋 번들(.bundle) 파일들을 먼저 CDN에 업로드한 뒤 마지막에 카탈로그 파일({Id}.hash 및 {Id}.json)을 업로드해야 합니다. 카탈로그가 먼저 갱신되면 클라이언트는 즉시 새 번들을 요청하지만 CDN에 아직 새 번들이 배포되지 않아 404 Not Found 또는 CRC Mismatch 오류를 마주하게 됩니다.


요약 및 권장 아키텍처 체크리스트

  1. 카탈로그 제어: Disable Catalog Update On Startup을 활성화하고 수동 파이프라인으로 제어하십시오.
  2. CRC 옵션 설정: Addressables 그룹의 Asset Bundle CRC에서 다운로드 번들과 캐시 번들의 검사 여부를 정하고, 목표 플랫폼에서 성능을 측정하십시오. CRC를 보안 변조 방지 수단으로 간주하지 마십시오.
  3. 복구 로직 구비: 네트워크 단절이나 CDN 캐시 불일치에 대비하여 ClearDependencyCacheAsync와 지수 백오프 기반 재시도 루프를 구현하십시오.
  4. CDN 배포 순서 준수: 번들 바이너리를 CDN에 완전히 퍼징/업로드한 후 최종 단계에서 .hash 및 .json을 릴리스하십시오.