모바일 앱에 온디바이스 LLM 탑재하기: Llama 3.2, Gemma 3, Phi-4 mini 실전 비교

모바일

온디바이스 AILlamaGemmaPhi모바일 AI

이 글은 누구를 위한 것인가

  • 서버 API 없이 AI 기능을 구현하고 싶은 모바일 개발자
  • 사용자 데이터를 외부 서버에 보내지 않는 프라이버시 중심 앱을 만드는 팀
  • 온디바이스 LLM의 실제 성능과 제약을 파악하고 싶은 분

들어가며

1년 전만 해도 온디바이스 LLM은 "가능하지만 실용적이지 않다"는 평가가 많았다. 응답이 너무 느리거나, 모델 품질이 기대에 미치지 못하거나, 디바이스 발열이 심했다.

2026년 봄 상황이 달라졌다. Meta의 Llama 3.2 1B/3B, Google의 Gemma 3 시리즈(270M, 1B, 4B), Microsoft의 Phi-4 mini(3.8B) — 경량 모델들이 실용적인 품질 수준에 도달했다. iPhone 15 이상, 최신 안드로이드 플래그십에서 초당 20~40 토큰으로 생성이 가능하다.

오프라인 번역, 음성 어시스턴트, 문서 요약, 이미지 캡션 — 이 기능들을 서버 없이, API 키 없이, 인터넷 연결 없이 구현할 수 있게 됐다. 이 글에서는 각 모델의 특성과 모바일 앱에서의 구현 방법을 실전 코드와 함께 다룬다.


1. 왜 온디바이스 LLM인가

클라우드 API vs 온디바이스

항목클라우드 API온디바이스 LLM
인터넷 필요필수불필요
응답 지연네트워크 포함 (500ms~3s)디바이스 성능에 따라 (100ms~2s)
비용토큰당 과금없음 (초기 모델 다운로드만)
데이터 프라이버시외부 서버 전송디바이스 내 처리
모델 품질최고 수준경량 모델 한계 있음
모델 크기 제한없음디바이스 스토리지/RAM 제한

온디바이스가 특히 유리한 케이스:

  • 오프라인 환경: 비행기 모드, 지하, 해외 로밍 중에도 동작
  • 프라이버시 민감 데이터: 의료 기록, 개인 일기, 금융 정보
  • 낮은 지연 시간: 타이핑 자동완성, 실시간 음성 인식 후처리
  • 비용 통제: 토큰 비용 없이 무제한 사용

2. 모델 비교: Llama 3.2, Gemma 3, Phi-4 mini

Llama 3.2 (Meta)

Meta가 2024년 출시한 모바일 특화 버전이다. 1B와 3B 두 가지 크기가 있다.

Llama 3.2 1B
- 모델 크기: ~2GB (4-bit 양자화 기준)
- 강점: 빠른 추론, 기본적인 텍스트 생성
- 약점: 복잡한 추론, 정확한 지식
- 적합한 용도: 자동완성, 간단한 분류, 감성 분석

Llama 3.2 3B
- 모델 크기: ~2.5GB (4-bit 양자화 기준)
- 강점: 1B 대비 품질 향상, 다국어 지원
- 약점: 1B 대비 느림
- 적합한 용도: 번역, 요약, Q&A

다국어 지원이 특히 강점이다. 한국어 포함 8개 언어를 공식 지원한다.

Gemma 3 (Google)

Google이 2025년 출시한 모바일/엣지 최적화 시리즈다. 270M의 극소형 모델부터 4B까지 선택지가 다양하다.

Gemma 3 270M
- 모델 크기: ~200MB
- 강점: 극도로 빠름, 작은 용량
- 약점: 품질 제한
- 적합한 용도: 텍스트 분류, 짧은 자동완성

Gemma 3 1B
- 모델 크기: ~800MB
- 강점: 빠름, 적절한 품질
- 적합한 용도: 일반 텍스트 생성, 요약

Gemma 3 4B
- 모델 크기: ~3.5GB
- 강점: 비전 기능 (이미지 이해)
- 적합한 용도: 이미지 캡션, 멀티모달 작업

Gemma 3 4B는 이미지도 이해할 수 있는 멀티모달 모델이다. 사진을 찍으면 자동으로 설명을 생성하는 기능에 적합하다.

Phi-4 mini (Microsoft)

Microsoft Research가 만든 3.8B 모델이다. 크기 대비 추론 능력이 뛰어나다.

Phi-4 mini
- 모델 크기: ~2.5GB (4-bit 양자화)
- 강점: 수학, 코드, 논리 추론에서 같은 크기 모델 중 최상위
- 약점: 다국어 지원 제한 (영어 중심)
- 적합한 용도: 코딩 어시스턴트, 수학 문제, 논리 추론

코딩 관련 기능(코드 설명, 버그 찾기, 코드 생성)에서는 3B 이하 모델 중 가장 높은 품질을 보인다.

모델 선택 가이드

빠른 속도, 작은 용량 → Gemma 3 270M 또는 1B
다국어 지원 (한국어 포함) → Llama 3.2 3B
이미지 이해 필요 → Gemma 3 4B
코드/수학/추론 → Phi-4 mini
균형잡힌 범용 → Llama 3.2 3B 또는 Phi-4 mini

3. iOS 구현: Core ML + llama.cpp

방법 1: Apple의 Core ML + Foundation Models

Apple은 iOS 18.1부터 온디바이스 AI를 위한 Foundation Models 프레임워크를 제공한다. 별도 모델 다운로드 없이 Apple Intelligence 모델을 활용할 수 있다.

import FoundationModels

// Apple의 온디바이스 모델 사용 (iOS 18.1+)
class OnDeviceAI {
    let session = LanguageModelSession()

    func summarize(text: String) async throws -> String {
        let prompt = "다음 텍스트를 3문장으로 요약하세요:\n\n\(text)"
        let response = try await session.respond(to: prompt)
        return response.content
    }

    func classify(text: String, categories: [String]) async throws -> String {
        let categoryList = categories.joined(separator: ", ")
        let prompt = "다음 텍스트를 [\(categoryList)] 중 하나로 분류하세요. 카테고리 이름만 답하세요.\n\n\(text)"
        let response = try await session.respond(to: prompt)
        return response.content.trimmingCharacters(in: .whitespacesAndNewlines)
    }
}

방법 2: llama.cpp 래퍼 (커스텀 모델)

Apple Foundation Models 대신 Llama, Gemma 등 특정 모델을 직접 실행하려면 llama.cpp 기반 iOS 래퍼를 사용한다.

import LLaMA  // llama.cpp Swift 바인딩

class CustomModelRunner {
    private var llamaContext: LlamaContext?
    private let modelPath: String

    init(modelName: String) {
        // 앱 번들에 포함하거나 앱 실행 후 다운로드
        self.modelPath = Bundle.main.path(forResource: modelName, ofType: "gguf")
            ?? FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
                .first!.appendingPathComponent("\(modelName).gguf").path
    }

    func loadModel() throws {
        var params = llama_context_default_params()
        params.n_ctx = 2048      // 컨텍스트 크기
        params.n_threads = 4     // CPU 스레드 수

        let model = llama_load_model_from_file(modelPath, llama_model_default_params())
        llamaContext = llama_new_context_with_model(model, params)
    }

    func generate(prompt: String, maxTokens: Int = 200) async -> AsyncStream<String> {
        AsyncStream { continuation in
            Task.detached(priority: .userInitiated) {
                // 실제 추론 실행 (메인 스레드 블로킹 방지)
                let tokens = self.tokenize(prompt)
                for _ in 0..<maxTokens {
                    guard let token = self.nextToken(tokens) else { break }
                    let text = self.tokenToString(token)
                    continuation.yield(text)
                    if token == EOS_TOKEN { break }
                }
                continuation.finish()
            }
        }
    }
}

모델 다운로드 관리

앱 번들에 2~3GB 모델을 포함하면 앱스토어 심사가 어렵다. 첫 실행 시 다운로드하는 방식이 현실적이다.

class ModelDownloader: ObservableObject {
    @Published var progress: Double = 0
    @Published var isDownloaded = false

    func downloadModel(modelName: String) async {
        let modelURL = URL(string: "https://your-cdn.com/models/\(modelName).gguf")!
        let destination = documentsDirectory.appendingPathComponent("\(modelName).gguf")

        // 이미 다운로드됐으면 스킵
        if FileManager.default.fileExists(atPath: destination.path) {
            isDownloaded = true
            return
        }

        // 백그라운드 다운로드 (앱이 종료돼도 계속)
        let config = URLSessionConfiguration.background(withIdentifier: "model-download")
        let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
        let task = session.downloadTask(with: modelURL)
        task.resume()
    }
}

4. Android 구현: MediaPipe LLM Inference API

Google의 MediaPipe LLM Inference API가 Android에서의 온디바이스 LLM 실행을 쉽게 만들었다.

import com.google.mediapipe.tasks.genai.llminference.LlmInference

class OnDeviceLLM(private val context: Context) {

    private var llmInference: LlmInference? = null

    fun initialize(modelPath: String) {
        val options = LlmInference.LlmInferenceOptions.builder()
            .setModelPath(modelPath)
            .setMaxTokens(1024)
            .setTopK(40)
            .setTemperature(0.8f)
            .setRandomSeed(42)
            .build()

        llmInference = LlmInference.createFromOptions(context, options)
    }

    // 스트리밍 응답 (실시간 텍스트 표시)
    fun generateStream(
        prompt: String,
        onPartialResult: (String) -> Unit,
        onComplete: (String) -> Unit
    ) {
        val fullResponse = StringBuilder()

        llmInference?.generateResponseAsync(prompt) { partialResult, done ->
            partialResult?.let {
                fullResponse.append(it)
                onPartialResult(it)
            }
            if (done) {
                onComplete(fullResponse.toString())
            }
        }
    }

    fun release() {
        llmInference?.close()
        llmInference = null
    }
}
// ViewModel에서 사용
class ChatViewModel(application: Application) : AndroidViewModel(application) {

    private val llm = OnDeviceLLM(application)
    val messages = mutableStateListOf<Message>()
    var isGenerating by mutableStateOf(false)

    init {
        viewModelScope.launch(Dispatchers.IO) {
            // 앱 시작 시 모델 초기화 (백그라운드)
            llm.initialize(getModelPath())
        }
    }

    fun sendMessage(userText: String) {
        messages.add(Message(userText, isUser = true))
        isGenerating = true

        val currentResponse = Message("", isUser = false)
        messages.add(currentResponse)

        viewModelScope.launch(Dispatchers.IO) {
            llm.generateStream(
                prompt = buildPrompt(userText),
                onPartialResult = { token ->
                    withContext(Dispatchers.Main) {
                        // 실시간 스트리밍 업데이트
                        val lastIndex = messages.lastIndex
                        messages[lastIndex] = messages[lastIndex].copy(
                            text = messages[lastIndex].text + token
                        )
                    }
                },
                onComplete = {
                    withContext(Dispatchers.Main) {
                        isGenerating = false
                    }
                }
            )
        }
    }
}

5. 성능 최적화: 실제로 빠르게 만들기

양자화 선택

같은 모델도 양자화 방식에 따라 크기와 속도가 크게 달라진다.

[Llama 3.2 3B 양자화별 비교 (iPhone 15 기준)]

FP16 (원본): 6.4GB, ~8 tokens/sec  → 실용성 낮음
Q8_0:        3.3GB, ~15 tokens/sec → 고품질 필요 시
Q4_K_M:      2.0GB, ~28 tokens/sec → 추천 (품질/속도 균형)
Q2_K:        1.3GB, ~35 tokens/sec → 품질 저하 있음

대부분의 경우 Q4_K_M이 최적의 균형점이다.

첫 응답 지연 줄이기

모델이 처음 응답을 시작하기까지 걸리는 시간(프리필 시간)이 사용자 경험에 크게 영향을 미친다.

// 프롬프트를 짧게 유지
// 나쁜 예: 너무 긴 시스템 프롬프트
let systemPrompt = """
당신은 매우 친절하고 도움이 되는 AI 어시스턴트입니다.
항상 정확하고 상세한 답변을 제공합니다.
사용자의 질문을 잘 이해하고...
(200 토큰짜리 시스템 프롬프트)
"""

// 좋은 예: 간결한 시스템 프롬프트
let systemPrompt = "친절한 AI 어시스턴트입니다."
// 결과를 미리 캐싱
class ResponseCache {
    private var cache = [String: String]()

    func get(_ prompt: String) -> String? {
        cache[prompt.hashValue.description]
    }

    func set(_ prompt: String, response: String) {
        // 캐시 크기 제한
        if cache.count > 100 { cache.removeAll() }
        cache[prompt.hashValue.description] = response
    }
}

6. 현실적인 한계와 대응

온디바이스 LLM이 실용화됐지만, 한계도 명확하다.

한계 1: 지식 한계 경량 모델은 최신 정보와 전문 지식에서 오류율이 높다. 검색 기능이나 RAG를 결합해서 지식 기반 보완이 필요하다.

한계 2: 발열과 배터리 지속적인 LLM 추론은 디바이스 발열과 배터리 소모를 일으킨다. 긴 대화보다 짧은 단위 작업에 적합하다.

// 연속 추론 제한으로 발열 관리
class ThermalManager {
    private var inferenceCount = 0
    private val MAX_CONTINUOUS = 5

    fun shouldPause(): Boolean {
        inferenceCount++
        if (inferenceCount >= MAX_CONTINUOUS) {
            inferenceCount = 0
            return true  // 잠깐 쉬어가기
        }
        return false
    }
}

한계 3: 모델 다운로드 경험 첫 실행 시 2~3GB 다운로드는 사용자 이탈을 유발할 수 있다. Wi-Fi 전용 다운로드, 단계적 기능 활성화, 명확한 다운로드 이유 설명이 필요하다.


맺으며

온디바이스 LLM은 "연구 수준"에서 "프로덕션 가능"으로 전환점을 넘었다. 클라우드 API를 대체하는 것이 목표가 아니다. 인터넷이 없어도 동작해야 하거나, 사용자 데이터를 서버에 보내기 어려운 상황, 토큰 비용 없이 AI를 무제한 제공하고 싶은 케이스에서 온디바이스 LLM이 진가를 발휘한다.

시작은 가볍게 하자. Gemma 3 1B나 Llama 3.2 1B로 텍스트 분류나 짧은 자동완성 기능부터 구현해보고, 사용자 반응을 보면서 확장하는 것이 현실적이다. 경량 모델의 한계를 먼저 파악하고, 어떤 작업이 온디바이스에서 충분히 잘 동작하는지 확인하는 것이 중요하다.