본문 바로가기

AI 개발

Firebase AI Logic + Gemini 실전 가이드 4편 — App Check, Vertex AI 전환, Remote Config, 모니터링, 비용 최적화

반응형

3편까지 개발 환경에서 Gemini를 붙였습니다. 4편은 실제 사용자에게 배포하기 전 해야 하는 것들입니다. App Check 없이 공개하면 누구나 내 API 할당량을 소진할 수 있습니다. Remote Config 없이 배포하면 모델 deprecation 때마다 앱 업데이트가 강제됩니다.

[4편 핵심 요약]
→ App Check: Gemini API를 인가된 앱만 사용하도록 보호 — 공개 배포 전 필수
→ 프로바이더: Android=Play Integrity / iOS=App Attest / Web=reCAPTCHA Enterprise
→ Vertex AI 전환: GoogleAIBackend() → VertexAIBackend() 한 줄 교체 — Blaze 플랜 필요
→ Remote Config: 모델명·파라미터·프롬프트를 앱 업데이트 없이 원격 변경
→ 프로덕션 모델명: auto-updated alias(gemini-3.1-flash-lite) 금지 → 고정 버전 사용
→ AI 모니터링: Firebase 콘솔 AI Logic 탭 → 요청수/지연/에러/토큰 대시보드
→ 비용 알림: Blaze 플랜 전환 후 Budget Alert 반드시 설정
→ 개발/스테이징/프로덕션: 별도 Firebase 프로젝트 필수

실전 1 — Firebase App Check 설정

공개 앱에 App Check 없으면 누구나 앱을 리버스 엔지니어링해서 내 Gemini 할당량을 소진할 수 있습니다. 공식 문서가 "가능한 빨리, 개발 중에도" 설정을 강권하는 이유입니다.

Android — Play Integrity

// app/build.gradle.kts
dependencies {
    implementation(platform("com.google.firebase:firebase-bom:34.13.0"))
    implementation("com.google.firebase:firebase-ai")
    implementation("com.google.firebase:firebase-appcheck-playintegrity")  // 추가
}
// MyApplication.kt — Application 클래스에서 초기화
import com.google.firebase.Firebase
import com.google.firebase.appcheck.appCheck
import com.google.firebase.appcheck.playintegrity.PlayIntegrityAppCheckProviderFactory
import com.google.firebase.initialize

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        // Firebase 초기화
        Firebase.initialize(this)

        // App Check 초기화 — Play Integrity (프로덕션)
        Firebase.appCheck.installAppCheckProviderFactory(
            PlayIntegrityAppCheckProviderFactory.getInstance()
        )
    }
}
// 개발 환경에서는 DebugAppCheckProviderFactory 사용
// AndroidManifest.xml에 Application 등록 필수
// <application android:name=".MyApplication" ...>

// 개발 중 디버그 토큰 활성화 (BuildConfig 활용)
if (BuildConfig.DEBUG) {
    Firebase.appCheck.installAppCheckProviderFactory(
        DebugAppCheckProviderFactory.getInstance()
    )
} else {
    Firebase.appCheck.installAppCheckProviderFactory(
        PlayIntegrityAppCheckProviderFactory.getInstance()
    )
}

// ⚠️ 주의: Play Integrity는 Play Store에 등록된 앱만 동작
// → 개발 중에는 디버그 토큰 필수

웹 — reCAPTCHA Enterprise

// lib/firebase.ts — reCAPTCHA Enterprise 추가
import { initializeApp } from "firebase/app";
import { initializeAppCheck, ReCaptchaEnterpriseProvider } from "firebase/app-check";
import { getAI, getGenerativeModel, GoogleAIBackend } from "firebase/ai";

const app = initializeApp(firebaseConfig);

// App Check 초기화 — reCAPTCHA Enterprise
// Firebase 콘솔에서 reCAPTCHA Enterprise 키 발급 필요
const appCheck = initializeAppCheck(app, {
  provider: new ReCaptchaEnterpriseProvider(
    process.env.NEXT_PUBLIC_RECAPTCHA_ENTERPRISE_SITE_KEY!
  ),
  isTokenAutoRefreshEnabled: true  // 토큰 자동 갱신
});

// AI Logic 초기화 (App Check 자동 연동)
const ai = getAI(app, { backend: new GoogleAIBackend() });
export const model = getGenerativeModel(ai, { model: "gemini-3.1-flash-lite" });
// 개발 환경 디버그 토큰 설정 (브라우저 콘솔에서 토큰 확인)
// next.config.ts
export default {
  env: {
    // 개발 환경에서만 활성화
    NEXT_PUBLIC_FIREBASE_APPCHECK_DEBUG_TOKEN:
      process.env.NODE_ENV === "development"
        ? process.env.APPCHECK_DEBUG_TOKEN
        : undefined,
  }
}

// lib/firebase.ts — 환경별 분기
if (process.env.NODE_ENV === "development") {
  // @ts-ignore — 전역 디버그 토큰 설정
  self.FIREBASE_APPCHECK_DEBUG_TOKEN =
    process.env.NEXT_PUBLIC_FIREBASE_APPCHECK_DEBUG_TOKEN || true;
}

Firebase 콘솔에서 App Check 강제 적용

[App Check 활성화 순서]

1. Firebase 콘솔 → App Check → 앱 등록
2. 프로바이더 선택 (Play Integrity / App Attest / reCAPTCHA)
3. 디버그 토큰 생성 (개발용)
4. "강제 적용" 전에 반드시 모니터링 기간 운영

강제 적용 전 모니터링:
→ App Check 탭에서 "검증됨" vs "검증 안 됨" 비율 확인
→ 정상 사용자 요청이 검증됨으로 나올 때까지 대기
→ 검증 안 됨이 일정 비율 이상 남아있으면 원인 분석 후 강제 적용

⚠️ 강제 적용 실수 시: 정상 사용자 요청도 차단될 수 있음
→ 모니터링 → 확인 → 강제 적용 순서 엄수

실전 2 — Vertex AI 백엔드 전환

유저가 늘어나거나 엔터프라이즈 SLA가 필요하면 Vertex AI로 전환합니다. 코드 변경은 최소화됩니다.

Android 전환

// 전환 전 (Gemini Developer API — 무료 티어)
val model = Firebase.ai(backend = GenerativeBackend.googleAI())
    .generativeModel("gemini-3.1-flash-lite")

// 전환 후 (Vertex AI — Blaze 플랜 필요)
val model = Firebase.ai(backend = GenerativeBackend.vertexAI())
    .generativeModel("gemini-3.1-flash-lite")
// ↑ 이 한 줄만 바꾸면 됨
// 위치(리전) 지정 — Vertex AI에서만 필요
val model = Firebase.ai(
    backend = GenerativeBackend.vertexAI(),
    location = "us-central1"  // 기본값. asia-northeast3(서울) 등 지정 가능
).generativeModel("gemini-3.1-flash-lite")

// Remote Config로 리전 동적 설정 (다음 실전에서 자세히)
val location = remoteConfig.getString("vertex_location")  // "us-central1"
val model = Firebase.ai(
    backend = GenerativeBackend.vertexAI(),
    location = location
).generativeModel(remoteConfig.getString("gemini_model"))

웹 전환

// 전환 전 (Gemini Developer API)
import { GoogleAIBackend } from "firebase/ai";
const ai = getAI(app, { backend: new GoogleAIBackend() });

// 전환 후 (Vertex AI)
import { VertexAIBackend } from "firebase/ai";
const ai = getAI(app, { backend: new VertexAIBackend() });
// ↑ import + 생성자 이름만 바꾸면 됨
[Gemini Developer API vs Vertex AI 선택 기준]

Gemini Developer API (무료 → Spark/Blaze):
→ 무료 티어로 시작 (신용카드 불필요)
→ 개인 프로젝트, 초기 스타트업
→ 빠른 프로토타이핑
→ 제한: 특정 모델만 지원, 데이터 위치 제한 없음
→ 무료 한도: gemini-3.1-flash-lite 1,500 req/일, 32,000 TPM

Vertex AI (Blaze 플랜 필요):
→ GCP 청구 계정 연결
→ 데이터 위치 요구사항 있는 경우 (EU, APAC 등)
→ 엔터프라이즈 SLA 필요
→ GCP 다른 서비스(BigQuery, Cloud Run 등)와 통합
→ 더 많은 모델 선택지
→ $300 GCP 신규 크레딧 사용 가능 (Gemini Developer API 무관)

실전 3 — Remote Config로 모델 동적 관리

앱 업데이트 없이 모델명, 파라미터, 프롬프트를 원격으로 변경합니다. 공식 문서가 "강력히 권장"하는 패턴입니다.

Android — Remote Config + Gemini 통합

// app/build.gradle.kts
dependencies {
    implementation(platform("com.google.firebase:firebase-bom:34.13.0"))
    implementation("com.google.firebase:firebase-ai")
    implementation("com.google.firebase:firebase-config")  // Remote Config 추가
}
// GeminiRepository.kt
import com.google.firebase.Firebase
import com.google.firebase.ai.ai
import com.google.firebase.ai.type.GenerativeBackend
import com.google.firebase.ai.type.generationConfig
import com.google.firebase.remoteconfig.remoteConfig
import com.google.firebase.remoteconfig.remoteConfigSettings

class GeminiRepository {

    private val remoteConfig = Firebase.remoteConfig

    init {
        // Remote Config 기본값 설정 (오프라인/초기값 보장)
        remoteConfig.setDefaultsAsync(
            mapOf(
                "gemini_model" to "gemini-3.1-flash-001",    // 안정 버전 고정
                "gemini_temperature" to 0.7,
                "gemini_max_tokens" to 1024,
                "gemini_system_prompt" to "당신은 친절한 AI 어시스턴트입니다.",
                "vertex_location" to "us-central1",
            )
        )

        // 최소 갱신 주기 설정 (기본 12시간)
        val settings = remoteConfigSettings {
            minimumFetchIntervalInSeconds = 3600  // 1시간
        }
        remoteConfig.setConfigSettingsAsync(settings)
    }

    // Remote Config 값 fetch + activate
    suspend fun fetchConfig() {
        remoteConfig.fetchAndActivate().await()
    }

    // Remote Config 값으로 모델 생성
    fun buildModel() = Firebase.ai(backend = GenerativeBackend.googleAI())
        .generativeModel(
            modelName = remoteConfig.getString("gemini_model"),
            generationConfig = generationConfig {
                temperature = remoteConfig.getDouble("gemini_temperature").toFloat()
                maxOutputTokens = remoteConfig.getLong("gemini_max_tokens").toInt()
            },
            systemInstruction = content {
                text(remoteConfig.getString("gemini_system_prompt"))
            }
        )
}

// ViewModel에서 사용
class ChatViewModel : ViewModel() {
    private val repository = GeminiRepository()
    private lateinit var model: GenerativeModel

    init {
        viewModelScope.launch {
            repository.fetchConfig()  // 앱 시작 시 최신 config 가져오기
            model = repository.buildModel()
        }
    }
}

Firebase 콘솔에서 Remote Config 설정

[콘솔에서 파라미터 설정]

Firebase 콘솔 → Remote Config → 파라미터 추가

파라미터 이름: gemini_model
기본값: gemini-3.1-flash-001    ← 안정 버전

조건 추가 예시:
→ 베타 테스터 그룹: gemini-3.1-pro-preview  (최신 모델 먼저 테스트)
→ 국가 = KR: gemini-3.1-flash-001          (지역별 모델 최적화)
→ 앱 버전 < 2.0: gemini-2.5-flash          (구버전 호환)

변경 후 "변경사항 게시" → 즉시 적용
→ 앱 업데이트 없이 모든 사용자에게 반영
// 웹 — Remote Config
import { getRemoteConfig, getValue, fetchAndActivate } from "firebase/remote-config";

const remoteConfig = getRemoteConfig(app);
remoteConfig.settings.minimumFetchIntervalMillis = 3600000; // 1시간

// 기본값 설정
remoteConfig.defaultConfig = {
  gemini_model: "gemini-3.1-flash-001",
  gemini_temperature: "0.7",
  gemini_max_tokens: "1024",
};

// fetch + activate
await fetchAndActivate(remoteConfig);

// 값으로 모델 초기화
const modelName = getValue(remoteConfig, "gemini_model").asString();
const temperature = parseFloat(getValue(remoteConfig, "gemini_temperature").asString());

const model = getGenerativeModel(ai, {
  model: modelName,
  generationConfig: { temperature, maxOutputTokens: 1024 }
});
[Remote Config 활용 시나리오]

1. 모델 deprecation 대응:
   gemini-2.5-flash 종료 예고 →
   Remote Config에서 gemini-3.1-flash-lite로 변경 →
   앱 업데이트 없이 즉시 모든 사용자 마이그레이션

2. 비용 최적화:
   트래픽 급증 시 gemini-3.1-pro → gemini-3.1-flash-lite로 다운그레이드
   비용 안정화 후 복원

3. A/B 테스트:
   temperature 0.5 vs 0.9 → 사용자 반응 측정
   Google Analytics 연동으로 지표 추적

4. 단계적 롤아웃:
   새 모델을 10% 사용자에게 먼저 배포
   이슈 없으면 100%로 확대

실전 4 — AI 모니터링 대시보드

[AI 모니터링 활성화 — Firebase 콘솔]

1. Firebase 콘솔 → AI Services → AI Logic
2. "AI monitoring" 탭 클릭
3. "Enable AI monitoring" 활성화
4. 최소 SDK 버전 요구사항 확인:
   - iOS: v11.13.0+
   - Android: v16.0.0+ (BoM: v33.14.0+)
   - Web: v11.8.0+
   - Flutter: v2.0.0+

활성화 후 5분 이내 데이터 수집 시작
별도 코드 변경 불필요
[AI 모니터링 대시보드에서 볼 수 있는 것]

요청 지표:
→ 시간별 요청 수 (정상/오류)
→ 응답 코드별 분포
→ API 메서드별 지연 시간
→ 전체 지연 시간 P50/P95/P99

토큰 지표:
→ 입력 토큰 수 (시간별 합계)
→ 출력 토큰 수 (시간별 합계)
→ 사용자별 평균 토큰 소비
→ 이상 토큰 소비 감지

개별 추적 (Trace):
→ 각 요청의 전체 생명주기
→ 입력 프롬프트 (샘플링)
→ 출력 응답
→ 토큰 수, 지연, 모델 버전

샘플링 설정:
→ 대용량 트래픽: 100% 수집 시 비용 발생
→ Google Cloud Observability Suite 사용량 기준
→ 권장: 10~30% 샘플링
// Android — AI 모니터링 데이터 수집 동의 확인
// 기본값: opt-in (enabled)
// 사용자 동의 기반 앱이라면 명시적 제어 가능

// 데이터 수집 비활성화 (GDPR 등 규제 환경)
Firebase.ai.dataCollectionEnabled = false

실전 5 — 비용 최적화 전략

[프리 티어 한도 (Gemini Developer API, 2026년 5월 기준)]

gemini-3.1-flash-lite:
→ 1,500 requests/일
→ 32,000 TPM (Tokens Per Minute)
→ 1,000,000 TPD (Tokens Per Day)

gemini-2.5-flash:
→ 500 requests/일
→ 250,000 TPM
→ 1,000,000 TPD

→ 무료 한도 초과 시: 429 오류 또는 Blaze 플랜 필요
// 비용 최적화 패턴 1: 토큰 예산 게이트
class TokenBudgetGuard(
    private val dailyBudget: Int = 500_000  // 일일 토큰 예산
) {
    private var usedToday = 0
    private var lastResetDate = ""

    suspend fun checkAndRecord(
        model: GenerativeModel,
        prompt: String
    ): Boolean {
        // 날짜 변경 시 카운터 리셋
        val today = LocalDate.now().toString()
        if (today != lastResetDate) {
            usedToday = 0
            lastResetDate = today
        }

        // 예상 토큰 수 확인 (실제 요청 전)
        val tokenCount = model.countTokens(prompt).totalTokens

        return if (usedToday + tokenCount > dailyBudget) {
            println("⚠️ 일일 토큰 예산 초과: $usedToday/$dailyBudget")
            false
        } else {
            usedToday += tokenCount
            true
        }
    }
}
// 비용 최적화 패턴 2: 모델 라우팅
// 태스크 복잡도에 따라 적합한 모델 선택

class ModelRouter {
    // 저비용 모델 (단순 태스크)
    private val flashLiteModel = Firebase.ai(backend = GenerativeBackend.googleAI())
        .generativeModel("gemini-3.1-flash-lite")

    // 고성능 모델 (복잡 태스크)
    private val flashModel = Firebase.ai(backend = GenerativeBackend.googleAI())
        .generativeModel("gemini-2.5-flash")

    fun selectModel(task: TaskType): GenerativeModel {
        return when (task) {
            TaskType.SIMPLE_QA,
            TaskType.CLASSIFICATION,
            TaskType.SHORT_SUMMARY -> flashLiteModel  // 저비용

            TaskType.COMPLEX_REASONING,
            TaskType.CODE_GENERATION,
            TaskType.LONG_DOCUMENT -> flashModel       // 고성능
        }
    }
}

enum class TaskType {
    SIMPLE_QA, CLASSIFICATION, SHORT_SUMMARY,
    COMPLEX_REASONING, CODE_GENERATION, LONG_DOCUMENT
}
// 비용 최적화 패턴 3: 응답 캐싱
// 동일 프롬프트 반복 요청 방지
import java.util.concurrent.ConcurrentHashMap

class GeminiResponseCache(
    private val model: GenerativeModel,
    private val maxCacheSize: Int = 100
) {
    private val cache = ConcurrentHashMap<String, String>()

    suspend fun generateWithCache(prompt: String): String {
        // 캐시 히트
        cache[prompt]?.let { return it }

        // 캐시 미스 → 실제 API 호출
        val response = model.generateContent(prompt).text ?: ""

        // 캐시 크기 제한
        if (cache.size >= maxCacheSize) {
            cache.remove(cache.keys.first())
        }
        cache[prompt] = response

        return response
    }
}
[Blaze 플랜 비용 관리 체크리스트]

☐ Budget Alert 설정 (Firebase 콘솔 → 사용량 및 청구)
  → 월 예산 설정
  → 50%, 90%, 100% 도달 시 이메일 알림
  → "하드 한도" 없음 — 알림만 전송됨

☐ 쿼터 설정 (Google Cloud 콘솔 → APIs & Services)
  → Gemini API 쿼터 설정
  → 사용자별 RPM 제한 (기본 100 RPM)
  → 과도한 사용 자동 차단

☐ AI 모니터링 활성화 (Firebase 콘솔)
  → 이상 토큰 소비 감지
  → 비정상 요청 패턴 조기 발견

☐ 프로덕션 모델명 고정 (auto-updated alias 금지)
  → gemini-3.1-flash-lite ❌ (alias, 자동 변경됨)
  → gemini-3.1-flash-lite-001 ✅ (고정 버전, 예측 가능)

실전 6 — 프로덕션 배포 전 최종 체크리스트

공식 문서 프로덕션 체크리스트 기반입니다.

[Firebase AI Logic 프로덕션 체크리스트]

보안:
☐ Firebase App Check 활성화 + 강제 적용
☐ API 키 앱 코드에 없음 확인 (.env 파일 git 제외)
☐ Firebase Security Rules 검토
☐ Cloud Storage 파일 접근 권한 제한 (멀티모달 사용 시)

모델 관리:
☐ 모델명 고정 버전 사용 (alias 금지)
☐ Remote Config로 모델명 관리 (앱 업데이트 없이 교체 가능)
☐ gemini-2.0-flash 사용 중이면 6/1 전 마이그레이션 완료
☐ Imagen 모델 사용 중이면 6/24 전 Nano Banana로 전환

프로젝트 분리:
☐ 개발용 Firebase 프로젝트 (dev)
☐ 스테이징용 Firebase 프로젝트 (staging)
☐ 프로덕션용 Firebase 프로젝트 (prod)
☐ 각 환경별 google-services.json / GoogleService-Info.plist 분리

비용 관리 (Blaze 플랜):
☐ Budget Alert 설정
☐ 사용자별 RPM 쿼터 설정
☐ AI 모니터링 활성화
☐ 샘플링 비율 설정

운영:
☐ 에러 처리 — 429(할당량 초과), 500(서버 오류) 사용자 안내
☐ 오프라인 대응 — 요청 실패 시 재시도 로직
☐ 긴 응답 타임아웃 설정
☐ 안전 설정(Safety Settings) 검토
// 에러 처리 — 프로덕션 필수
import com.google.firebase.ai.type.FirebaseAIException
import com.google.firebase.ai.type.FinishReason

fun handleGeminiError(error: Exception): String {
    return when (error) {
        is FirebaseAIException -> when {
            error.message?.contains("429") == true ->
                "요청이 너무 많습니다. 잠시 후 다시 시도해주세요."

            error.message?.contains("500") == true ->
                "서비스 오류가 발생했습니다. 잠시 후 다시 시도해주세요."

            error.message?.contains("SAFETY") == true ->
                "안전 정책에 의해 응답이 차단되었습니다."

            else -> "오류가 발생했습니다: ${error.message}"
        }
        else -> "알 수 없는 오류가 발생했습니다."
    }
}

// 응답 완료 이유 확인
fun checkFinishReason(response: GenerateContentResponse) {
    val finishReason = response.candidates.firstOrNull()?.finishReason
    when (finishReason) {
        FinishReason.STOP -> println("정상 완료")
        FinishReason.MAX_TOKENS -> println("토큰 한도 초과 — maxOutputTokens 증가 필요")
        FinishReason.SAFETY -> println("안전 필터 차단 — Safety Settings 검토")
        FinishReason.RECITATION -> println("저작권 필터 차단")
        else -> println("기타 종료: $finishReason")
    }
}

마무리 — 4편 시리즈 완결

[전체 시리즈 정리]

1편: Firebase 프로젝트 설정 + 첫 API 호출 (Android/웹 공통)
2편: Android 실전 — 스트리밍, 채팅, 멀티모달, 구조화 출력
3편: 웹 실전 — TypeScript/React 커스텀 훅, Next.js App Router
4편: 프로덕션 — App Check, Vertex AI 전환, Remote Config, 모니터링

[권장 도입 순서]
1. Gemini Developer API 무료 티어로 시작
2. 개발 중부터 App Check 설정
3. Remote Config로 모델명 관리
4. 트래픽 증가 시 Blaze 플랜 전환
5. AI 모니터링 활성화 + Budget Alert
6. 필요 시 Vertex AI로 백엔드 전환 (코드 한 줄)

✅ 프로덕션 배포 최소 요건
→ App Check 강제 적용
→ 모델명 고정 버전 사용
→ Remote Config로 모델 관리
→ 개발/프로덕션 Firebase 프로젝트 분리
→ 에러 처리 구현

관련 글

반응형