[OS 트러블 슈팅] Android 15~16 WebView Cold Start 크래시 분석 및 대응

마시마로·2026년 7월 1일

안녕하세요. 최근 Android 16 업데이트 기기(특히 삼성 One UI 8.0 기반 기기)에서 Cold Start 시 WebView 초기화 단계에서 앱이 즉시 종료(FC)되는 크래시 이슈를 마주했습니다.

Play Console Vitals 상에서 크래시 지수가 평소 대비 약 150,000% 급등할 정도로 심각했던 사안이었는데요. 앱 자체의 코드 변경이 없었던 상황에서 어떤 가설을 세우고 원인을 추적했는지, 그리고 제조사에 기술 문의를 하고 앱 레벨에서 방어 코드를 작성한 과정을 공유합니다.


1. 문제 현상

Android 16 업데이트 기기에서 프로세스가 새로 시작되는 Cold Start 시점에 앱이 즉시 종료되는 현상이 감지되었습니다.

  • 크래시 로그: android.app.ActivityThread.performLaunchActivity 단계에서 InflateException 및 NotFoundException 발생
  • 발생 시점: 레이아웃 XML에 선언된 커스텀 WebView가 인플레이션(Inflation)되어 즉시 초기화될 때 발생
  • 특이 사항: '영향을 받는 사용자'와 '이벤트'가 1:1로 매칭되며 대개 OS 업데이트 후 최초 1회성으로 집중 발생

2. 가설 수립 및 정밀 분석

처음 로그를 파악했을 때 ResourcesImpl 리다이렉션 키워드가 눈에 띄었습니다. 앱 코드는 동일한 상태인데 OS 버전이나 기기 환경에 따라 현상이 갈렸습니다. 그래서 내부 로직 결함보다는 OS 프레임워크와 리소스 매핑 쪽에 가설을 세우고 접근했습니다.

🔍 진짜 원인: Android 16의 리소스 참조 위반 정책 강화

구글과 제조사에 기술 문의를 해서 확인한 정확한 메커니즘은 이렇습니다.

  1. GMS(Google Mobile Services) 업데이트가 런타임에 일어날 때, 앱이 갱신된 리소스 경로를 즉각 반영하지 못하고 과거의 경로를 참조하는 일시적인 위반(Violation) 현상이 발생합니다.
  2. Android 15 이전까지는 이런 Race Condition이 발생해도 유연하게 넘어갔는데 Android 16부터는 리소스 참조 위반 정책이 강화되었습니다.
  3. 결과적으로 ResourcesImpl 리다이렉션이 실패하면 예외 처리 없이 프레임워크 단에서 앱 프로세스를 즉시 크래시(FC) 처리해 버리는 것이 문제의 본질이었습니다.

3. 단계별 대처 과정

문제 포착부터 제조사 소통, 내부 데이터 검증까지의 대처 흐름입니다.

  • [Phase 1] 이슈 최초 포착 및 리포트 (2025.09 ~ 12)
    Android 16이 처음 배포된 2025년 6월 무렵에는 크래시 수치가 높지 않았습니다. 그러다 9월부터 이상 수치가 보여서 Google Issue Tracker에 최초 리포트를 올리고 장기 모니터링을 시작했습니다.
  • [Phase 2] 기술 문의 및 정밀 분석 (2026.01 ~ 02)
    정식 업데이트 기기가 늘어나며 크래시가 급증하자 삼성 개발자 지원팀에 1차 기술 문의를 했습니다. 단순 앱 오류가 아님을 설명하려고 과거 관련 QA 리포트 분석 데이터를 근거로 제시했습니다.
  • [Phase 3] 로그 제출 및 이슈 공식화 (2026.03)
    내부에서 의도적으로 발생 시킨 dumpstate 로그를 함께 제출해 기술 문의를 구체화했고 플랫폼 단의 리소스 매핑 결함이라는 최종 피드백을 확인받았습니다.
  • [Phase 4] 효과 검증 (2026.03 ~ 현재)
    제조사의 수정 패치(One UI 8.5 이상)가 반영된 기기들을 위주로 Play Console 데이터를 모니터링해 크래시가 해소된 것을 확인했습니다.

4. 해결 방법 및 방어 코드

제조사 펌웨어 업데이트로 근본적으로 해소되는 문제입니다. 다만 사용자의 OS 업데이트 지연이나 파편화 환경을 고려해 앱 레벨에서 안전하게 구동될 수 있도록 방어 코드를 추가했습니다.

Application 레벨 방어 코드

Android 16에서는 GMS가 업데이트되면 GMS 패키지 경로가 랜덤하게 바뀝니다. 그런데 앱 프로세스는 이 바뀐 경로를 찾지 못한 채로 WebView 내부에서 GMS 리소스를 참조하다가 오류가 납니다.

그래서 Application 클래스의 onCreate() 시점에 createPackageContext로 GMS 패키지를 한 번 다시 로드합니다. GMS의 최신 패키지 경로를 앱 프로세스에 반영하려고 부르는 것이라 이 호출이 돌려주는 Context는 따로 쓰지 않습니다. 호출만 해도 경로가 재로드되기 때문입니다.

@SuppressLint("CheckResult")
override fun onCreate() {
    super.onCreate()
    initGmsPackageDependency() // GMS 의존성 컨텍스트 갱신 유도
    // ..기존 초기화 로직
}

/**
 * Android 16 대응 GMS 패키지 의존성 강제 등록
 * 런타임 리소스 참조 위반(Violation)으로 인한 크래시 방지용 방어 코드
 */
private fun initGmsPackageDependency() {
    // Android 16(SDK 36) 환경에서만 작동하도록 제한
    // VERSION_CODES.BAKLAVA = 36 (compileSdk 36 이상 필요). VANILLA_ICE_CREAM(V)은 35, 즉 Android 15입니다.
    if (Build.VERSION.SDK_INT != Build.VERSION_CODES.BAKLAVA) {
        DebuggingLog.d("[Application] GMS 의존성 등록 불필요 - SDK: ${Build.VERSION.SDK_INT}")
        return
    }

    try {
        // GMS 패키지 명을 기반으로 컨텍스트를 강제 로드하여 컨텍스트 갱신 유도
        // 반환값은 사용하지 않음: 바뀐 GMS 패키지 경로를 재로드하는 것이 목적
        createPackageContext(
            "com.google.android.gms", // GMS_PACKAGE_NAME
            CONTEXT_IGNORE_SECURITY or CONTEXT_INCLUDE_CODE
        )
        DebuggingLog.d("[Application] GMS 패키지 의존성 등록 성공")
    } catch (e: Throwable) {
        DebuggingLog.d("[Application] GMS 패키지 의존성 등록 실패: ${e.message}")
    }
}

처음 공개한 코드에는 이 분기가 Build.VERSION_CODES.V로 되어 있었습니다. V는 Android 15(SDK 35)라서 주석과 달리 Android 16 기기에서는 방어 코드가 실행되지 않습니다. 같은 코드를 쓰고 계셨다면 BAKLAVA로 바꿔 주세요.

마치며

새 안드로이드 플랫폼이 나올 때마다 리소스/보안 정책도 바뀌기 때문에 이번처럼 앱 코드를 건드리지 않았는데도 예측하기 어려운 사이드 이펙트가 생기곤 합니다.

이번 이슈에서는 원인 불명의 크래시 앞에서 정확한 로그(dumpstate) 분석과 기술 문의로 플랫폼 단의 변화를 빠르게 캐치했고 앱 레벨 방어 코드도 추가했습니다. targetSdk 36 전환이나 Android 16 환경에 대응하고 계신 개발자분들께 작은 도움이 되었으면 좋겠습니다.

profile
#Mobile #Android #iOS #Flutter #개발문화

0개의 댓글