[Android] 온디바이스 AI 개발기 - 6편: Play Asset Delivery로 모델 배포하기

1GB 모델을 앱에 어떻게 넣나 — 구글이 AI Gallery로 직접 보여준 방법

Posted by 동식이 블로그 on April 8, 2026

[Android] 온디바이스 AI 개발기 - 6편: Play Asset Delivery로 모델 배포하기

1GB짜리 모델 파일, 앱에 번들링할 수는 없다. 그럼 어떻게 배포하나.

이 글은

5편에서 1B 모델의 실제 한계를 정리했다. 이번엔 모델 파일 자체를 어떻게 앱에 제공하는지를 다룬다.

gemma3-1b-it-int4.litertlm은 약 1GB다. APK나 AAB에 번들링하는 건 처음부터 불가능하다. AAB의 base module 압축 다운로드 크기 제한이 200MB이기 때문이다.

방법은 크게 두 가지다.

  1. 직접 다운로드 — 앱 서버나 CDN에서 직접 받아오기
  2. Play Asset Delivery — Google Play 인프라를 활용해 배포

Google AI Gallery를 보면 답이 보인다

얘기를 시작하기 전에 Google AI Gallery 얘기를 잠깐 하고 싶다.

Google AI Gallery는 LiteRT-LM 모델들을 직접 체험해볼 수 있는 앱인데, 흥미로운 UX가 있다. 사용자가 모델을 직접 선택하고 다운로드할 수 있다.

Gemma 1B, Gemma 3B, Gemma 4B… 모델마다 용량이 다르고, 사용자가 원하는 걸 골라서 받는 구조다.

이게 그냥 UX 선택이 아닌 것 같다. 구글이 직접 만든 레퍼런스 앱이 이 방식을 채택했다는 건, 온디바이스 AI 모델은 이렇게 배포해라 는 가이드를 행동으로 보여주는 거라고 생각한다. 공식 문서보다 직관적인 레퍼런스다.

그리고 이 구조를 구현하기에 딱 맞는 게 Play Asset Delivery다.

Play Asset Delivery란

Play Asset Delivery(PAD)는 Google Play 인프라를 통해 대용량 에셋을 런타임에 배포할 수 있는 기능이다. 게임에서 대용량 리소스를 나눠서 제공하던 방식인데, LLM 모델 배포에도 그대로 쓸 수 있다.

직접 다운로드 vs Play Asset Delivery 비교

  직접 다운로드 Play Asset Delivery
인프라 직접 구축 (S3, CDN 등) Google Play 인프라
비용 트래픽 비용 발생 Play 정책 내 무료
무결성 검증 직접 구현 자동 처리
델타 업데이트 직접 구현 지원
구현 복잡도 낮음 중간
오프라인 설치 불가 설치 시 포함 가능
pack당 최대 크기 제한 없음 (서버 설정에 따라) 1.5GB

모델 업데이트가 잦거나 여러 모델을 제공할 계획이라면 PAD가 유리하다.

PAD 구성 방식

PAD는 에셋 전달 방식에 따라 세 가지로 나뉜다.

1
2
3
install-time  → 앱 설치 시 함께 설치 (용량 제한 있음)
fast-follow   → 설치 직후 자동 다운로드
on-demand     → 앱 실행 중 필요할 때 다운로드

모델 파일은 on-demand가 적합하다. 사용자가 AI 기능을 사용하려 할 때 그때 받는 구조다. AI Gallery처럼 여러 모델을 제공한다면 사용자가 선택한 모델만 받으면 된다.

구현

1. asset pack 모듈 생성

프로젝트에 asset pack 전용 모듈을 추가한다.

1
2
3
4
5
6
7
app/
ai-model-pack/          ← 새 모듈
  build.gradle.kts
  src/
    main/
      assets/
        gemma3-1b-it-int4.litertlm
1
2
3
4
5
6
7
8
9
10
11
// ai-model-pack/build.gradle.kts
plugins {
    id("com.android.asset-pack")
}

assetPack {
    packName.set("ai_model_pack")
    dynamicDelivery {
        deliveryType.set("on-demand")
    }
}
1
2
3
4
// app/build.gradle.kts
android {
    assetPacks += [":ai-model-pack"]
}

2. 의존성 추가

1
2
3
4
// app/build.gradle.kts
dependencies {
    implementation("com.google.android.play:asset-delivery-ktx:2.2.2")
}

3. 다운로드 요청

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class ModelDownloader(private val context: Context) {

    private val assetPackManager = AssetPackManagerFactory.getInstance(context)

    fun downloadModel(): Flow<ModelDownloadState> = callbackFlow {
        val packName = "ai_model_pack"

        val listener = AssetPackStateUpdateListener { state ->
            when (state.status()) {
                AssetPackStatus.DOWNLOADING -> {
                    val progress = state.bytesDownloaded() * 100 / state.totalBytesToDownload()
                    trySend(ModelDownloadState.Downloading(progress.toInt()))
                }
                AssetPackStatus.COMPLETED -> {
                    trySend(ModelDownloadState.Completed(getModelPath(packName)))
                    close()
                }
                AssetPackStatus.FAILED -> {
                    trySend(ModelDownloadState.Failed(state.errorCode()))
                    close()
                }
                AssetPackStatus.REQUIRES_USER_CONFIRMATION -> {
                    // 대용량 다운로드 시 사용자 확인 필요
                    assetPackManager.showConfirmationDialog(activity)
                }
                else -> Unit
            }
        }

        assetPackManager.registerListener(listener)
        assetPackManager.fetch(listOf(packName))

        awaitClose { assetPackManager.unregisterListener(listener) }
    }

    private fun getModelPath(packName: String): String {
        val location = assetPackManager.getPackLocation(packName)
        return "${location?.assetsPath()}/gemma3-1b-it-int4.litertlm"
    }
}

sealed interface ModelDownloadState {
    data class Downloading(val progress: Int) : ModelDownloadState
    data class Completed(val modelPath: String) : ModelDownloadState
    data class Failed(val errorCode: Int) : ModelDownloadState
}

4. 이미 다운로드됐는지 확인

1
2
3
4
suspend fun isModelReady(): Boolean {
    val location = assetPackManager.getPackLocation("ai_model_pack")
    return location != null
}

앱 시작 시점에 이미 다운로드된 모델이 있으면 바로 초기화하고, 없으면 다운로드 화면을 보여주면 된다.

여러 모델을 선택할 수 있게 하려면

AI Gallery처럼 사용자가 모델을 선택하는 구조라면, 모델마다 asset pack을 분리한다.

1
2
3
ai-model-gemma-1b/     → "ai_model_gemma_1b"
ai-model-gemma-3b/     → "ai_model_gemma_3b"
ai-model-gemma-4b/     → "ai_model_gemma_4b"

사용자가 선택한 모델의 pack만 fetch()하면 된다. 나머지는 다운로드하지 않는다.

이 구조가 AI Gallery가 보여주는 방식이고, 실제로 LLM을 앱에 배포할 때 가장 현실적인 방법이기도 하다. 구글이 직접 만든 앱이 이렇게 한다는 건, 이 패턴을 권장한다는 신호로 읽힌다.

주의할 점

테스트 환경

PAD는 Google Play를 통해 배포되는 구조라 로컬 개발 환경에서 테스트하기 까다롭다. 개발 중에는 adb로 모델을 직접 밀어 넣고, PAD 연동은 내부 테스트 트랙에서 별도로 검증하는 게 현실적이다.

1
adb push gemma3-1b-it-int4.litertlm /data/local/tmp/

용량 안내

1GB 다운로드를 사용자에게 예고 없이 시작하면 안 된다. Wi-Fi 환경 확인, 용량 안내, 사용자 동의 흐름이 필요하다.

저장 공간 확인

다운로드 전 기기 여유 공간을 확인하고 부족하면 안내해줘야 한다.

정리

  • 1GB 모델은 번들링 불가 → 런타임 배포 필요
  • Play Asset Delivery: Google Play 인프라 활용, 무결성 검증 자동
  • on-demand 방식으로 사용자가 필요할 때 다운로드
  • 여러 모델 제공 시 pack 분리 → 사용자가 선택한 것만 받는 구조
  • AI Gallery가 이 패턴의 레퍼런스 — 구글이 직접 보여주는 방법

이 시리즈는 여기서 마무리. 1편부터 읽어주신 분들 감사합니다.

참고사이트