Android 앱 오류 추적과 품질 관리 — Firebase Crashlytics, ANR 분석, Google Play Android Vitals 실전 가이드
Android 앱을 개발할 때 가장 어려운 문제 중 하나는 개발자의 기기에서는 정상적으로 동작하지만 실제 사용자 기기에서는 오류가 발생하는 상황입니다.
예를 들어 다음과 같은 문제입니다.
특정 삼성 갤럭시 기기에서만 앱이 종료됨
Android 버전 업데이트 이후 크래시가 증가함
SQLite 데이터베이스를 초기화하는 과정에서 앱이 멈춤
RecyclerView에서 특정 데이터를 표시할 때 예외가 발생함
WebView에서 JavaScript 통신 중 오류가 발생함
광고 로딩 직후 앱이 종료됨
사용자가 앱이 멈춘다고 신고했지만 개발자는 재현하지 못함
이러한 문제는 Logcat만으로 해결하기 어렵습니다.
Logcat은 개발자가 직접 연결한 기기의 로그를 확인하는 데 유용하지만, 실제 사용자의 모든 기기에서 발생한 문제를 자동으로 수집해 주지는 않습니다.
이때 활용할 수 있는 도구가 Firebase Crashlytics와 Google Play Android Vitals입니다.
이번 글에서는 Kotlin 기반 Android 앱에서 크래시와 ANR을 수집하고, 원인을 분석하며, 앱 품질을 지속적으로 개선하는 방법을 알아보겠습니다.
작성 기준: 2026년 10월. Firebase SDK, Gradle 플러그인, Google Play Console의 지표 및 정책은 변경될 수 있으므로 실제 적용 시 공식 문서를 확인해야 합니다.
1. Android 앱에서 발생하는 오류 유형
먼저 앱에서 발생하는 문제를 구분해야 합니다.
| 유형 | 설명 | 대표적인 원인 |
|---|---|---|
| Crash | 앱이 비정상적으로 종료됨 | 처리되지 않은 예외 |
| ANR | 앱이 일정 시간 응답하지 않음 | 메인 스레드 차단 |
| Non-fatal Error | 앱이 종료되지는 않았지만 오류 발생 | API 오류, 데이터 처리 실패 |
| OOM | 메모리 부족으로 문제가 발생함 | 대용량 Bitmap, 메모리 누수 |
| UI 오류 | 화면 표시나 사용자 조작 문제 | 상태 관리, 레이아웃 오류 |
| 성능 저하 | 앱이 느려지거나 버벅거림 | 과도한 I/O, 렌더링 지연 |
이 중 Crash와 ANR은 Google Play에서 앱 품질을 평가할 때 특히 중요한 지표입니다.
하지만 모든 예외를 크래시로 기록해서는 안 됩니다.
예를 들어 네트워크 연결 실패는 사용자의 환경에서 정상적으로 발생할 수 있는 상황입니다.
이런 오류는 사용자에게 적절한 메시지를 표시하고 복구하도록 설계해야 합니다.
2. Crashlytics와 Android Vitals의 차이
Firebase Crashlytics와 Google Play Android Vitals는 모두 앱 품질을 분석하는 데 도움이 됩니다.
하지만 제공하는 정보와 역할에는 차이가 있습니다.
| 구분 | Firebase Crashlytics | Google Play Android Vitals |
|---|---|---|
| 제공 서비스 | Firebase | Google Play Console |
| 주요 목적 | 오류 수집과 원인 분석 | 실제 사용자 환경의 앱 품질 분석 |
| Crash 분석 | 지원 | 지원 |
| ANR 분석 | 지원 범위와 환경에 따라 제공 | 지원 |
| 커스텀 로그 | 지원 | 제한적 |
| 커스텀 키 | 지원 | 직접적인 동일 기능 없음 |
| Non-fatal 예외 | 직접 기록 가능 | 일반적인 커스텀 예외 수집 도구는 아님 |
| 배포 연동 | Firebase SDK 필요 | Google Play 배포 앱 대상 |
| 주요 활용 | 상세 디버깅 | 품질 지표 및 기기별 문제 파악 |
두 서비스를 경쟁 관계로 보기보다는 함께 사용하는 것이 좋습니다.
Crashlytics에서는 오류의 구체적인 발생 상황을 분석하고, Android Vitals에서는 실제 사용자에게 미치는 영향을 확인할 수 있습니다.
3. Firebase 프로젝트 생성하기
Crashlytics를 사용하려면 Firebase 프로젝트가 필요합니다.
기본적인 설정 순서는 다음과 같습니다.
Firebase Console 접속
Firebase 프로젝트 생성
Android 앱 등록
앱의 Application ID 입력
google-services.json다운로드Android 프로젝트에 Firebase SDK 추가
Crashlytics 활성화
Firebase Console 주소는 다음과 같습니다.
https://console.firebase.google.com/
Android 앱 등록 시 입력하는 패키지 이름은 실제 Application ID와 일치해야 합니다.
Product Flavor를 사용하는 프로젝트에서는 Flavor별 Application ID가 다를 수 있으므로 주의해야 합니다.
4. google-services.json 파일 추가
Firebase에서 다운로드한 google-services.json 파일을 Android 프로젝트에 추가합니다.
일반적인 위치는 다음과 같습니다.
app/
├── google-services.json
├── build.gradle.kts
└── src/Product Flavor별로 Firebase 프로젝트나 앱 등록 정보가 다르다면 다음과 같이 분리할 수도 있습니다.
app/
└── src/
├── bird/
│ └── google-services.json
└── fish/
└── google-services.json중요한 점은 Firebase에 등록된 Application ID와 실제 빌드되는 앱의 Application ID가 일치해야 한다는 것입니다.
google-services.json에는 Firebase 프로젝트 설정 정보가 포함됩니다.
이 파일의 API Key는 일반적으로 앱 식별 및 Firebase 서비스 설정에 사용되는 값이며, 서버 관리자 비밀 키와 동일한 개념은 아닙니다.
다만 Firebase 프로젝트의 보안 규칙과 API 제한 설정은 별도로 관리해야 합니다.
5. Firebase Crashlytics 의존성 추가
Kotlin DSL을 사용하는 Android 프로젝트에서는 Google Services와 Crashlytics Gradle 플러그인을 설정해야 합니다.
프로젝트의 플러그인 관리 방식에 따라 설정 위치가 다를 수 있습니다.
예를 들어 루트 build.gradle.kts에서 다음과 같이 선언할 수 있습니다.
plugins {
id("com.google.gms.google-services") version "<version>" apply false
id("com.google.firebase.crashlytics") version "<version>" apply false
}앱 모듈에서는 플러그인을 적용합니다.
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("com.google.gms.google-services")
id("com.google.firebase.crashlytics")
}의존성에는 Firebase BoM을 사용할 수 있습니다.
dependencies {
implementation(
platform(
"com.google.firebase:firebase-bom:<version>"
)
)
implementation(
"com.google.firebase:firebase-crashlytics"
)
}<version>에는 현재 프로젝트와 호환되는 안정 버전을 지정합니다.
Firebase BoM은 호환되는 Firebase 라이브러리 버전을 관리하는 데 도움이 되지만, Gradle 플러그인 버전까지 자동으로 관리하는 것은 아닙니다.
6. Crashlytics 인스턴스 사용하기
Crashlytics는 다음과 같이 사용할 수 있습니다.
val crashlytics =
FirebaseCrashlytics.getInstance()예를 들어 오류가 발생했을 때 다음과 같이 기록할 수 있습니다.
try {
loadLocalDatabase()
} catch (e: Exception) {
FirebaseCrashlytics
.getInstance()
.recordException(e)
}이 코드는 앱을 종료시키지 않는 예외를 Crashlytics에 기록하는 예시입니다.
다만 예외를 기록한 뒤 아무 처리도 하지 않으면 앱의 상태가 잘못 유지될 수 있습니다.
따라서 오류의 종류에 따라 사용자에게 메시지를 표시하거나 작업을 중단하는 등의 처리가 필요합니다.
7. Fatal Crash와 Non-fatal Error 구분하기
Crashlytics에서는 치명적인 크래시와 개발자가 기록한 비치명적 예외를 구분할 수 있습니다.
Fatal Crash
처리되지 않은 예외로 앱이 비정상 종료되는 상황입니다.
throw IllegalStateException(
"Unexpected application state"
)이 코드는 실행하면 앱이 종료될 수 있으므로 실제 사용자에게 배포되는 코드에 테스트 목적으로 포함해서는 안 됩니다.
Non-fatal Error
예외가 발생했지만 앱이 계속 실행되는 상황입니다.
try {
updateBirdDatabase()
} catch (e: SQLiteException) {
FirebaseCrashlytics
.getInstance()
.recordException(e)
showDatabaseError()
}이런 방식은 오류를 기록하면서도 사용자에게 복구 방법을 안내할 수 있습니다.
하지만 모든 네트워크 실패나 일반적인 사용자 입력 오류를 Non-fatal로 기록하면 중요한 문제를 찾기 어려워질 수 있습니다.
따라서 기록할 오류의 기준을 정하는 것이 좋습니다.
8. Crashlytics 커스텀 로그 활용하기
Crashlytics에서는 오류 발생 전후의 상황을 파악하기 위해 로그를 남길 수 있습니다.
FirebaseCrashlytics
.getInstance()
.log("Bird detail screen opened")예를 들어 SQLite 업데이트 과정에서 다음과 같이 기록할 수 있습니다.
val crashlytics =
FirebaseCrashlytics.getInstance()
crashlytics.log(
"Database update started"
)
try {
updateDatabase()
crashlytics.log(
"Database update completed"
)
} catch (e: Exception) {
crashlytics.log(
"Database update failed"
)
crashlytics.recordException(e)
}이렇게 하면 오류가 발생하기 전 어떤 작업이 실행되었는지 파악하는 데 도움이 됩니다.
단, Crashlytics 로그는 일반적인 Logcat 로그와 목적이 다릅니다.
로그의 수와 저장 용량에는 제한이 있으므로 필요한 정보만 기록해야 합니다.
9. 커스텀 키로 오류 상황 기록하기
Crashlytics에서는 커스텀 키를 이용해 앱 상태를 기록할 수 있습니다.
예를 들어 다음과 같은 정보입니다.
현재 화면
데이터베이스 버전
동기화 작업 단계
앱 기능 상태
선택된 Product Flavor
val crashlytics =
FirebaseCrashlytics.getInstance()
crashlytics.setCustomKey(
"screen",
"bird_detail"
)
crashlytics.setCustomKey(
"database_version",
12
)
crashlytics.setCustomKey(
"sync_status",
"updating"
)이러한 정보는 특정 조건에서만 발생하는 오류를 분석할 때 유용합니다.
하지만 사용자 이름, 이메일, 전화번호, 인증 토큰 등 민감한 정보를 커스텀 키에 저장해서는 안 됩니다.
또한 커스텀 키는 개별 함수의 지역변수처럼 자동으로 사라지는 값이 아닙니다.
따라서 화면이나 작업 상태가 바뀌면 값을 적절히 갱신해야 합니다.
10. SQLite 오류를 Crashlytics로 추적하기
SQLite를 직접 사용하는 앱에서는 데이터베이스 관련 오류가 발생할 수 있습니다.
대표적으로 다음과 같은 상황입니다.
데이터베이스 파일이 정상적으로 복사되지 않음
테이블이나 컬럼이 존재하지 않음
DB 스키마 버전이 맞지 않음
데이터베이스 파일이 손상됨
SQL 문법 오류 발생
데이터베이스가 잠겨 작업이 실패함
예를 들어 다음과 같이 오류를 기록할 수 있습니다.
suspend fun loadBirds(): List<Bird> {
return withContext(Dispatchers.IO) {
try {
database.getBirds()
} catch (e: CancellationException) {
throw e
} catch (e: SQLiteException) {
FirebaseCrashlytics
.getInstance()
.apply {
setCustomKey(
"database_operation",
"load_birds"
)
recordException(e)
}
throw e
}
}
}여기서 중요한 점은 예외를 기록한 뒤 다시 전달한다는 것입니다.
Repository가 예외를 임의로 무시하고 빈 목록을 반환하면 실제 데이터베이스 오류를 단순히 데이터가 없는 상황으로 오해할 수 있습니다.
오류 복구 정책은 ViewModel이나 상위 계층에서 명확하게 결정하는 것이 좋습니다.
11. Retrofit 네트워크 오류 기록하기
Retrofit을 사용하는 앱에서는 네트워크 오류가 자주 발생할 수 있습니다.
하지만 모든 IOException을 Crashlytics에 기록할 필요는 없습니다.
예를 들어 사용자가 지하철에서 네트워크 연결이 끊어진 경우는 일반적인 환경 문제일 수 있습니다.
반면 특정 API에서 반복적으로 JSON 파싱 오류가 발생한다면 개발자가 확인해야 하는 문제일 가능성이 높습니다.
try {
val birds = api.getBirds()
saveBirds(birds)
} catch (e: CancellationException) {
throw e
} catch (e: IOException) {
// 일반적인 네트워크 실패 처리
} catch (e: JsonParseException) {
FirebaseCrashlytics
.getInstance()
.recordException(e)
throw e
}이 예제에서는 네트워크 실패와 데이터 파싱 오류를 구분합니다.
실제 프로젝트에서는 HTTP 상태 코드, 요청 종류, 서버 응답 형식에 따라 기록 정책을 세분화할 수 있습니다.
12. WebView 오류 추적하기
WebView를 사용하는 앱에서는 JavaScript 오류와 Android 네이티브 오류를 구분해야 합니다.
예를 들어 JavaScript 실행 결과를 Android로 전달하는 과정에서 오류가 발생할 수 있습니다.
webView.evaluateJavascript(
"document.title"
) { result ->
Log.d(
"WebView",
"Result: $result"
)
}하지만 evaluateJavascript()의 콜백 결과가 JavaScript 예외를 항상 명확하게 전달하는 것은 아닙니다.
JavaScript 오류를 추적하려면 WebChromeClient의 콘솔 메시지 처리 등을 활용할 수 있습니다.
webView.webChromeClient =
object : WebChromeClient() {
override fun onConsoleMessage(
consoleMessage: ConsoleMessage
): Boolean {
if (
consoleMessage.messageLevel() ==
ConsoleMessage.MessageLevel.ERROR
) {
Log.e(
"WebView",
consoleMessage.message()
)
}
return false
}
}이 예제는 Logcat에 오류를 출력합니다.
실제 서비스에서 Crashlytics로 전달하려면 로그 수집 범위와 개인정보 노출 가능성을 검토해야 합니다.
특히 외부 웹페이지의 콘솔 메시지를 무조건 Crashlytics에 전송하는 것은 권장하지 않습니다.
13. ANR이란?
ANR은 Application Not Responding의 약자입니다.
앱이 일정 시간 동안 사용자 입력이나 시스템 요청에 응답하지 못할 때 발생할 수 있습니다.
대표적인 원인은 다음과 같습니다.
메인 스레드에서 SQLite 쿼리 실행
메인 스레드에서 파일 복사
긴 반복문 실행
동기 네트워크 요청
스레드 간 잠금 대기
BroadcastReceiver에서 오래 걸리는 작업 수행
WebView 초기화 과정에서 메인 스레드 차단
ANR은 일반적인 Exception과 다릅니다.
따라서 try-catch로 ANR을 해결할 수는 없습니다.
ANR을 해결하려면 메인 스레드가 왜 응답하지 못했는지 확인해야 합니다.
14. Android Vitals에서 ANR 확인하기
Google Play Console에서는 Android Vitals를 통해 실제 사용자 환경에서 발생한 ANR 정보를 확인할 수 있습니다.
일반적으로 다음과 같은 정보를 활용할 수 있습니다.
ANR 발생 추세
영향을 받은 사용자 비율
Android 버전
기기 모델
앱 버전
ANR 관련 스택 정보
Google Play Console의 메뉴 구성과 제공 지표는 변경될 수 있습니다.
또한 Android Vitals는 Google Play에서 수집되는 데이터와 적용 범위에 따라 표시되므로 모든 오류가 즉시 나타나는 것은 아닙니다.
특히 사용자에게 영향을 주는 ANR 비율은 앱 품질을 평가할 때 중요한 지표입니다.
15. 메인 스레드에서 SQLite 실행하지 않기
다음과 같은 코드는 주의해야 합니다.
val birds = database.getAllBirds()이 코드가 메인 스레드에서 실행된다면 데이터가 많거나 저장소 응답이 느린 환경에서 화면이 멈출 수 있습니다.
Coroutine을 이용해 I/O 작업을 분리하는 것이 좋습니다.
viewModelScope.launch {
val birds = withContext(
Dispatchers.IO
) {
database.getAllBirds()
}
_birds.value = birds
}이렇게 하면 SQLite 조회를 I/O 스레드에서 수행할 수 있습니다.
다만 Coroutine을 사용한다고 모든 ANR이 해결되는 것은 아닙니다.
예를 들어 메인 스레드에서 대량의 데이터를 가공하거나 RecyclerView에 과도한 변경을 전달하면 여전히 성능 문제가 발생할 수 있습니다.
16. StrictMode로 메인 스레드 작업 확인하기
개발 중에는 StrictMode를 이용해 메인 스레드에서 발생하는 부적절한 작업을 확인할 수 있습니다.
예를 들어 Application 클래스에서 다음과 같이 설정할 수 있습니다.
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog()
.build()
)
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
.detectAll()
.penaltyLog()
.build()
)
}
}
}StrictMode는 개발 중 문제를 발견하는 데 유용합니다.
하지만 StrictMode에서 경고가 발생했다고 해서 반드시 ANR이 발생한다는 의미는 아닙니다.
또한 모든 ANR 원인을 StrictMode로 확인할 수 있는 것도 아닙니다.
따라서 실제 ANR 분석에는 스택 트레이스와 Android Studio Profiler 등의 도구를 함께 사용하는 것이 좋습니다.
17. R8 난독화와 크래시 분석
릴리스 빌드에서는 R8을 이용해 코드를 축소하고 난독화할 수 있습니다.
예를 들어 다음과 같은 클래스가 있다고 가정하겠습니다.
class BirdRepository {
fun getBirds() {
// ...
}
}난독화 이후에는 클래스와 메서드 이름이 짧게 변경될 수 있습니다.
이 경우 크래시 로그에 다음과 같이 표시될 수 있습니다.
at a.b.c.a(Unknown Source)이런 로그만으로는 실제 오류가 발생한 코드를 파악하기 어렵습니다.
이때 필요한 파일이 **R8 매핑 파일(mapping.txt)**입니다.
18. mapping.txt 파일이란?
R8 매핑 파일에는 난독화 전후의 클래스 및 메서드 이름을 연결하는 정보가 포함됩니다.
일반적인 출력 경로는 다음과 같습니다.
app/build/outputs/mapping/release/mapping.txtProduct Flavor를 사용하면 경로가 달라질 수 있습니다.
app/build/outputs/mapping/birdRelease/mapping.txt매핑 파일은 해당 릴리스 빌드와 정확하게 일치해야 합니다.
서로 다른 버전의 매핑 파일을 사용하면 스택 트레이스를 올바르게 복원할 수 없습니다.
따라서 릴리스마다 매핑 파일을 보관하는 것이 좋습니다.
19. Crashlytics 매핑 파일 업로드
Firebase Crashlytics Gradle 플러그인을 정상적으로 구성하면 지원되는 빌드 과정에서 R8 매핑 파일 업로드를 자동화할 수 있습니다.
하지만 실제 업로드가 성공했는지는 확인해야 합니다.
특히 다음과 같은 경우 주의해야 합니다.
Product Flavor별 릴리스 빌드
CI/CD 환경에서 빌드
Firebase 프로젝트가 여러 개인 경우
빌드 작업을 분리한 경우
Crashlytics 관련 Gradle 작업을 비활성화한 경우
GitHub Actions로 AAB를 빌드한다면 Crashlytics 매핑 파일 업로드 작업이 함께 실행되는지 확인하는 것이 좋습니다.
매핑 파일 업로드가 실패하면 릴리스 크래시 로그가 난독화된 상태로 표시될 수 있습니다.
20. NDK 크래시와 심볼 파일
Android 앱에 C/C++ 네이티브 라이브러리가 포함되어 있다면 Java/Kotlin 크래시와 별도로 네이티브 크래시가 발생할 수 있습니다.
예를 들어 다음과 같은 문제가 있습니다.
Segmentation Fault
네이티브 메모리 접근 오류
잘못된 포인터 사용
네이티브 라이브러리 충돌
이러한 오류를 분석하려면 네이티브 디버그 심볼이 필요할 수 있습니다.
Crashlytics에서 네이티브 크래시를 수집하려면 Crashlytics NDK 지원과 관련 설정을 추가해야 합니다.
또한 Google Play Console에서도 네이티브 디버그 심볼을 업로드해 크래시 분석에 활용할 수 있습니다.
단, Kotlin과 Java만 사용하는 앱이라면 NDK 크래시 설정이 반드시 필요한 것은 아닙니다.
21. Crashlytics에서 사용자 식별 정보 관리
Crashlytics는 오류 분석을 위해 사용자 식별자를 설정할 수 있습니다.
FirebaseCrashlytics
.getInstance()
.setUserId("internal_user_id")하지만 사용자 이메일이나 전화번호 같은 개인정보를 그대로 설정하는 것은 피하는 것이 좋습니다.
가능하면 서비스 내부에서 생성한 비식별 식별자를 사용하는 것이 좋습니다.
또한 사용자가 로그아웃하거나 계정이 변경되면 사용자 식별자 관리 정책을 검토해야 합니다.
Crashlytics에 수집되는 정보는 앱의 개인정보처리방침 및 Google Play Data Safety 신고 내용과도 일치해야 합니다.
22. Crashlytics 데이터 수집 동의 관리
Crashlytics는 앱 설정에 따라 자동 데이터 수집 여부를 제어할 수 있습니다.
예를 들어 다음과 같은 API를 사용할 수 있습니다.
FirebaseCrashlytics
.getInstance()
.setCrashlyticsCollectionEnabled(false)다만 이 설정의 적용 시점과 영속성, 이전에 수집된 데이터 처리 방식은 Firebase SDK의 동작을 확인해야 합니다.
또한 앱이 Crashlytics를 사용하는 경우 해당 데이터 수집이 개인정보처리방침과 Google Play Data Safety에 어떻게 반영되는지 검토해야 합니다.
특히 여러 국가에 앱을 배포한다면 적용되는 개인정보 보호 규정을 함께 확인해야 합니다.
23. Android Vitals에서 우선 확인할 지표
Google Play Console의 Android Vitals에서는 다양한 품질 지표를 확인할 수 있습니다.
대표적으로 다음 항목을 살펴볼 수 있습니다.
| 지표 | 확인 목적 |
|---|---|
| 사용자 인지 크래시 비율 | 실제 사용자에게 영향을 주는 크래시 |
| 사용자 인지 ANR 비율 | 사용자가 경험한 앱 응답 없음 |
| 기기별 오류 | 특정 제조사 또는 모델 문제 |
| Android 버전별 오류 | OS 호환성 문제 |
| 앱 버전별 추세 | 신규 릴리스 이후 품질 변화 |
| 배터리 및 성능 관련 지표 | 과도한 리소스 사용 여부 |
Google Play는 일부 핵심 품질 지표에 대해 기준값을 운영합니다.
다만 기준값과 평가 방식은 변경될 수 있으므로 특정 수치를 코드나 운영 문서에 고정하기보다 현재 Play Console의 안내를 확인하는 것이 좋습니다.
24. 릴리스 이후 오류가 증가했을 때
새로운 앱 버전을 배포한 이후 크래시가 증가했다고 가정하겠습니다.
이때는 다음 순서로 확인하는 것이 좋습니다.
1단계: 오류 증가 시점 확인
새 버전 배포 시점과 오류 증가 시점이 일치하는지 확인합니다.
2단계: 영향을 받은 앱 버전 확인
모든 버전에서 발생하는지, 특정 버전에서만 발생하는지 확인합니다.
3단계: 기기 및 Android 버전 확인
특정 제조사나 Android 버전에서 집중적으로 발생하는지 확인합니다.
4단계: 스택 트레이스 확인
오류가 발생한 클래스와 메서드를 확인합니다.
5단계: 최근 변경 코드 확인
해당 버전에서 변경한 기능을 검토합니다.
6단계: 재현 및 수정
가능하면 동일한 조건에서 오류를 재현합니다.
7단계: 수정 버전 배포
문제를 수정한 뒤 테스트 트랙에서 검증하고 배포합니다.
단, 오류가 증가했다고 해서 항상 최신 코드가 원인인 것은 아닙니다.
서버 API 변경, 외부 SDK, OS 업데이트 등도 영향을 줄 수 있습니다.
25. 크래시 우선순위 결정하기
모든 오류를 동일한 우선순위로 처리할 필요는 없습니다.
예를 들어 다음과 같은 기준을 사용할 수 있습니다.
| 우선순위 | 상황 |
|---|---|
| 매우 높음 | 앱 시작 직후 반복 크래시 |
| 높음 | 주요 기능 사용 중 크래시 |
| 높음 | 특정 기기에서 다수 사용자에게 발생 |
| 중간 | 특정 조건에서만 발생하는 기능 오류 |
| 중간 | 복구 가능한 비치명적 오류 |
| 낮음 | 사용자 영향이 거의 없는 드문 오류 |
다만 발생 빈도가 낮더라도 사용자 데이터 손실이나 보안 문제로 이어지는 오류라면 높은 우선순위로 처리해야 합니다.
따라서 단순히 크래시 횟수만 보는 것이 아니라 사용자 영향도를 함께 판단해야 합니다.
26. Google Play 내부 테스트와 Crashlytics 연동
앱을 운영 환경에 배포하기 전에 내부 테스트 트랙을 활용하면 좋습니다.
예를 들어 다음과 같은 흐름을 구성할 수 있습니다.
코드 수정
│
▼
GitHub Actions
│
├── 빌드
├── Unit Test
└── AAB 생성
│
▼
Google Play 내부 테스트
│
▼
테스터 실행
│
├── Crashlytics 오류 확인
└── 기능 테스트
│
▼
문제 수정
│
▼
운영 배포이렇게 하면 실제 사용자에게 배포하기 전에 일부 오류를 발견할 수 있습니다.
하지만 내부 테스트에서 문제가 없었다고 해서 운영 환경에서도 문제가 없다고 보장할 수는 없습니다.
기기 종류, Android 버전, 네트워크 환경, 사용자 데이터 상태가 다양하기 때문입니다.
27. Crashlytics 테스트 시 주의사항
Crashlytics가 정상적으로 동작하는지 확인하려면 테스트용 크래시를 발생시킬 수 있습니다.
예를 들어 다음과 같은 코드를 사용할 수 있습니다.
throw RuntimeException(
"Test Crashlytics Crash"
)하지만 이 코드는 실제로 앱을 종료시킵니다.
따라서 반드시 테스트 빌드에서만 실행해야 합니다.
테스트 후에는 코드를 제거하거나 실행되지 않도록 해야 합니다.
또한 Crashlytics의 크래시 보고서는 앱 재실행 이후 전송될 수 있으며, Firebase Console에 즉시 표시되지 않을 수 있습니다.
28. 실제 프로젝트에서 추천하는 오류 관리 구조
Kotlin + MVVM 기반 프로젝트에서는 오류를 다음과 같이 분리해 관리할 수 있습니다.
Activity / Fragment
│
▼
ViewModel
│
▼
Repository
│
├── Retrofit
├── SQLite
└── WebView 관련 데이터 처리
│
▼
Error Reporter
│
├── Logcat (Debug)
└── Crashlytics (Release)예를 들어 오류 기록 기능을 인터페이스로 분리할 수 있습니다.
interface ErrorReporter {
fun recordException(
throwable: Throwable
)
fun log(
message: String
)
}Crashlytics 구현체는 다음과 같습니다.
class CrashlyticsErrorReporter :
ErrorReporter {
private val crashlytics =
FirebaseCrashlytics.getInstance()
override fun recordException(
throwable: Throwable
) {
crashlytics.recordException(throwable)
}
override fun log(
message: String
) {
crashlytics.log(message)
}
}이렇게 하면 Repository가 Firebase SDK에 직접 의존하지 않도록 구성할 수 있습니다.
또한 Unit Test에서는 가짜 ErrorReporter를 사용해 오류 기록 동작을 검증할 수 있습니다.
29. 앱 품질 관리 체크리스트
Firebase Crashlytics 프로젝트 연결
실제 Application ID와 Firebase 설정 일치
Fatal Crash 수집 확인
Non-fatal 예외 기록 기준 정의
커스텀 로그와 키 활용
개인정보 및 인증 토큰 기록 방지
SQLite 오류 추적
Retrofit 네트워크 오류 분류
WebView 오류 기록 정책 검토
ANR 관련 메인 스레드 작업 점검
StrictMode 디버그 빌드 적용
R8 매핑 파일 업로드 확인
네이티브 라이브러리 사용 시 심볼 파일 검토
Android Vitals 정기 확인
앱 버전별 크래시 추세 분석
내부 테스트 트랙에서 오류 확인
개인정보처리방침 및 Data Safety 신고 검토
수정 버전 배포 후 오류 감소 여부 확인
30. 마무리
Android 앱 품질 관리는 앱을 출시한 이후부터 더욱 중요해집니다.
개발자의 기기에서 정상적으로 동작하더라도 실제 사용자 환경에서는 예상하지 못한 문제가 발생할 수 있기 때문입니다.
이러한 문제를 해결하려면 오류를 수집하고, 분석하고, 수정한 뒤 다시 검증하는 과정이 필요합니다.
실무에서는 다음 원칙을 추천합니다.
Crashlytics로 실제 크래시와 비치명적 예외를 추적합니다.
Android Vitals로 사용자에게 영향을 주는 품질 지표를 확인합니다.
SQLite, Retrofit, WebView 오류를 유형별로 구분합니다.
ANR은 메인 스레드와 스택 트레이스를 중심으로 분석합니다.
R8 매핑 파일을 관리해 릴리스 오류를 분석할 수 있도록 합니다.
민감한 개인정보와 인증 토큰을 오류 로그에 남기지 않습니다.
새 버전을 배포한 뒤 오류 추세가 개선되었는지 확인합니다.
특히 개인 개발자는 모든 사용자 기기를 직접 테스트하기 어렵습니다.
따라서 Crashlytics와 Android Vitals를 함께 사용하면 제한된 개발 환경에서도 실제 사용자에게 발생하는 문제를 파악하는 데 도움이 됩니다.
앱 품질 관리의 핵심은 오류가 전혀 발생하지 않는다고 가정하는 것이 아니라, 오류가 발생했을 때 원인을 빠르게 찾고 사용자 영향을 줄일 수 있는 체계를 만드는 것입니다.
참고 자료
Firebase Crashlytics 공식 문서
https://firebase.google.com/docs/crashlyticsAndroid Crashlytics 시작하기
https://firebase.google.com/docs/crashlytics/get-started?platform=androidCrashlytics 커스텀 로그 및 키
https://firebase.google.com/docs/crashlytics/customize-crash-reports?platform=androidAndroid Developers — ANR 진단
https://developer.android.com/topic/performance/vitals/anrAndroid Developers — Android Vitals
https://developer.android.com/topic/performance/vitalsAndroid Developers — StrictMode
https://developer.android.com/reference/android/os/StrictModeAndroid Developers — R8 코드 축소 및 난독화
https://developer.android.com/build/shrink-codeGoogle Play Console 도움말 — Android Vitals
https://support.google.com/googleplay/android-developer/answer/9844486
태그: Android, Kotlin, Firebase, Crashlytics, Android Vitals, ANR, Crash, SQLite, Retrofit, WebView, R8, ProGuard, Google Play Console, MVVM, 앱 품질 관리