Crash는 처리되지 않은 예외 또는 치명적인 시스템 오류로 인한 모바일 애플리케이션의 비정상 종료입니다. Firebase Crashlytics에 따르면 약 2%의 사용자가 매일 크래시를 경험하며, 각 크래시는 리텐션을 10~20% 감소시킵니다. 크래시의 원인과 예방 방법을 이해하는 것은 모든 모바일 개발자에게 필수적인 기술입니다.
핵심 요점
Crash는 애플리케이션 코드에서 처리되지 않은 예외나 치명적인 시스템 신호로 인해 발생하는 애플리케이션의 비정상 종료입니다. 시스템 또는 가상 머신(JVM, ART)이 치명적인 상태(NullPointerException, IndexOutOfBoundsException, OutOfMemoryError)를 감지하면 즉시 프로세스를 중지하고 메모리에서 언로드합니다. 사용자는 시스템 오류 알림 없이 애플리케이션이 갑자기 종료되는 것을 보게 됩니다. Google에 따르면 크래시 프리율이 99% 미만인 앱은 월간 활성 사용자의 최대 20%를 잃습니다.
Android에서는 크래시 처리 메커니즘이 데스크톱 시스템과 다릅니다. 스택 트레이스가 있는 디버그 대화상자 대신 Android는 세부 정보를 저장하지 않고 프로세스를 종료합니다. 크래시 정보 수집은 프로세스가 종료되기 전에 Thread.setDefaultUncaughtExceptionHandler를 통해 예외를 가로채는 타사 라이브러리(Crashlytics, Sentry, Bugsnag)의 역할입니다.
iOS는 치명적인 오류를 처리하기 위해 NSException 및 Mach exceptions와 유사한 메커니즘을 사용합니다. 처리되지 않은 예외가 발생하면 시스템이 애플리케이션을 종료하고 보고서가 .crash 파일로 저장됩니다. iOS에서 크래시를 수집하려면 Crashlytics와의 통합 또는 Xcode Organizer를 통한 내장 보고서가 필요합니다.
다섯 가지 범주의 크래시가 모바일 애플리케이션의 모든 오류 중 90%를 차지합니다. 각 유형을 이해하면 프로덕션에서 문제를 더 빠르게 진단하고 수정하는 데 도움이 됩니다.
NullPointerException(NPE)은 모든 Java/Kotlin 애플리케이션에서 가장 흔한 크래시 유형입니다. null인 객체의 메서드를 호출하거나 필드에 액세스하려고 할 때 발생합니다. 일반적인 시나리오: 화면 회전 시 초기화되지 않은 Activity 필드, JSON 역직렬화 중 서버의 null 응답, RecyclerView 어댑터를 통한 부주의한 탐색.
Kotlin은 null 안전 타입을 통해 언어 수준에서 NPE 문제를 해결합니다. String?은 명시적 확인 없이 사용할 수 없습니다. 그러나 Java 호환성과 리플렉션은 여전히 위험을 만듭니다. @NonNull 및 @Nullable 어노테이션을 사용하고 정적 분석 도구에서 strictNullChecks를 활성화하세요.
fun safeLength(text: String?): Int {
return text?.length ?: 0 // null의 안전한 처리
}
IndexOutOfBoundsException은 리스트나 배열의 존재하지 않는 인덱스에 액세스할 때 발생합니다. 일반적인 시나리오: 어댑터와 동기화 없이 RecyclerView에서 요소 제거, 잠금 없이 ArrayList의 멀티스레드 수정, ViewPager에서 잘못된 위치 계산. ConcurrentModificationException은 컬렉션을 반복하고 동시에 수정할 때 발생하는 가까운 사촌입니다.
멀티스레드 액세스에는 CopyOnWriteArrayList 또는 java.util.concurrent의 잠금 없는 컬렉션을 사용하세요. UI 동기화의 경우 이전 목록과 새 목록의 차이를 안전하고 효율적으로 계산하는 DiffUtil을 사용합니다.
ClassCastException은 객체를 호환되지 않는 타입으로 변환할 때 발생합니다. Android에서 일반적인 원인: RecyclerView에서 잘못된 ViewHolder 타입(적절한 getItemViewType 없이 다른 셀 타입), 탐색 중 잘못된 Fragment 캐스팅, 다른 클래스 버전의 Serializable 객체.
as? 연산자를 통해 Kotlin의 안전한 캐스트를 사용하세요. 타입이 호환되지 않으면 null을 반환합니다. Java에서는 캐스트 전에 instanceof로 확인하세요. Parcelable 객체의 경우 각 클래스에서 CREATOR를 선언해야 합니다.
IllegalStateException은 객체의 부적절한 상태에서 메서드를 호출했음을 나타냅니다. Android의 일반적인 예 — onSaveInstanceState 후 getSupportFragmentManager()로, Fragment의 commit()이 허용되지 않는 경우입니다. 또 다른 일반적인 경우는 이미 닫힌 대화상자에서 dismiss()를 호출하는 것입니다.
FragmentManager 작업 전에 생명주기 상태를 확인하세요. commitAllowingStateLoss()는 상태 손실이 중요하지 않다고 확신하는 경우에만 사용하세요. Kotlin에서는 타입 수준에서 유효하지 않은 상태를 제거하는 DSL 스타일 빌더를 만듭니다.
Native Crash는 네이티브 C/C++ 코드에서 메모리 위반(널 포인터 역참조, 이중 해제, 스택 버퍼 오버플로)으로 인해 발생합니다. Android에서는 NDK 라이브러리, 게임 엔진(Unity, Unreal) 및 시스템 종속성에서 이러한 크래시가 발생합니다. Native Crash는 Thread.setDefaultUncaughtExceptionHandler로 가로챌 수 없으며 프로세스를 즉시 종료합니다.
네이티브 크래시 진단을 위해 minidump 파일(Breakpad) 또는 Android tombstone을 사용하세요. Firebase Crashlytics는 NDK SDK를 통해 네이티브 크래시 수집을 지원합니다. iOS에서는 PLCrashReporter를 사용하여 유사한 문제를 해결합니다.
세 가지 도구가 모바일 크래시 리포팅 시장을 지배하고 있습니다. 각 도구는 스택 트레이스 수집, 앱 버전별 집계 및 새 크래시 알림을 제공합니다.
Crashlytics는 Firebase 생태계의 일부인 모바일 애플리케이션용 가장 인기 있는 크래시 리포터입니다. 스택 트레이스, 기기 정보, OS 버전 및 사용자 지정 키를 자동으로 수집합니다. Firebase Console 및 Gradle Plugin을 통해 통합하는 데 10분이 걸립니다. Crashlytics는 실시간 로그(Logcat) 및 사용자 트랙도 지원합니다.
FirebaseCrashlytics.getInstance()
.setCustomKey("current_screen", "ProfileFragment")
FirebaseCrashlytics.getInstance()
.log("User tapped login button")
Sentry는 더 유연한 필터링 시스템과 90개 이상의 플랫폼 지원을 갖춘 Crashlytics의 대안입니다. Firebase와 달리 Sentry는 엄격한 데이터 요구 사항이 있는 기업을 위해 자체 호스팅 서버를 제공합니다. Sentry는 분산 추적, breadcrumbs 및 CI/CD 파이프라인 통합을 지원합니다.
Bugsnag는 심각도 기반 알림을 지원하는 점에서 차별화됩니다. 크래시를 critical, error 및 warning으로 분류합니다. Microsoft의 AppCenter는 소규모 프로젝트를 위한 기본 기능을 갖춘 무료 도구입니다. 둘 다 Android, iOS, React Native 및 Flutter를 지원합니다.
크래시 분석은 발생한 사건의 전체 그림을 재구성하는 과정입니다. 스택 트레이스는 마지막 오류 지점만 보여주지만 문제로 이어진 컨텍스트는 제공하지 않습니다. 전문적인 접근 방식에는 네 단계가 포함됩니다.
첫 번째 단계 — 스택 트레이스를 읽습니다. 예외가 발생한 클래스, 메서드 및 코드 라인을 식별합니다. 상위 프레임에서 하위 프레임으로 호출 체인을 추적합니다: 스택의 마지막 줄이 크래시 위치이고 위쪽 줄이 호출 순서입니다. 프로덕션 빌드에서는 난독화 해제(ProGuard/R8 매핑)가 필수입니다.
두 번째 단계 — 기기 컨텍스트. Crashlytics는 기기 모델, OS 버전, 사용 가능한 메모리 및 앱 버전을 보여줍니다. 예를 들어 Android 11이 설치된 Samsung Galaxy S10에서만 발생하는 크래시는 일반적인 코드 오류가 아닌 특정 One UI 버전의 문제를 나타냅니다.
세 번째 단계 — 테스트 기기에서 재현. 크래시가 일관되게 재현되지 않는 경우 사용자에게 정확한 단계를 묻거나 Remote Config를 사용하여 문제가 있는 코드 섹션 앞에 로깅을 추가합니다. 일부 사용자를 대상으로 수정 사항을 AB 테스트하면 해결책을 확인하는 데 도움이 됩니다.
네 번째 단계 — 수정 후 모니터링. 수정 사항을 게시한 후 3~5일 동안 크래시율을 모니터링합니다. 크래시가 완전히 사라지면 수정이 성공한 것입니다. 빈도가 감소했지만 0이 되지 않은 경우 — 별도 분석이 필요한 두 번째 시나리오가 존재합니다.
체계적인 접근 방식을 통한 크래시 예방에는 정적 분석 도구, 필수 에지 케이스 테스트 및 애플리케이션의 모든 수준에서 적절한 오류 처리가 포함됩니다.
Detekt(Kotlin) 및 Lint(Android)는 컴파일 시간에 잠재적인 문제(사용되지 않는 변수, 잠재적 NPE, 잘못된 API 사용)를 찾습니다. 이러한 도구를 오류 임계값과 함께 CI 파이프라인에 포함하세요. 예를 들어 30개 이상의 경고 또는 오류 차단 설정이 있는 Detekt는 빌드를 통과시키지 않습니다.
단위 테스트를 통한 주요 사용 시나리오의 커버리지는 회귀 크래시에 대한 기본적인 보호입니다. 에지 케이스(null 값, 빈 목록, 유효하지 않은 JSON)로 데이터 모델, ViewModel 및 UseCase 계층을 테스트하세요. Espresso 또는 Compose Test를 통한 UI 테스트는 인증, 결제, 온보딩과 같은 중요한 흐름을 다룹니다.
애플리케이션을 설계할 때 한 모듈의 오류가 전체 화면을 크래시시키지 않도록 합니다. ViewModel 수준에서 폴백 상태와 함께 catch 블록을 사용합니다: 목록 대신 플레이스홀더 표시, 오프라인 시 캐시된 데이터, 로드 오류 시 폴백 이미지. 이렇게 하면 잠재적인 크래시가 제어된 UX 시나리오로 전환됩니다.
단계적 롤아웃은 Google Play 및 App Store의 표준 관행입니다: 새 버전이 5%, 그 다음 20%, 그 다음 100%의 사용자에게 1~3일 간격으로 배포됩니다. 각 단계에서 크래시율이 모니터링됩니다: 크래시 프리율이 99.5% 아래로 떨어지면 롤아웃이 자동으로 중지됩니다. Firebase Remote Config를 사용하면 새 버전을 게시하지 않고도 문제가 있는 기능을 비활성화할 수 있습니다.
CI의 Renovate 또는 Dependabot이 알려진 취약점 및 중요 버그에 대해 라이브러리를 자동으로 확인합니다. 단일 의존성을 업데이트하여 전체 크래시 클래스를 제거할 수 있습니다. 그러나 프로덕션에 롤아웃하기 전에 스테이징 환경에서 업데이트를 테스트하세요 — 새 라이브러리 버전에 호환되지 않는 변경 사항이 포함될 수 있습니다.
자주 묻는 질문
아니요. 일부 크래시는 개발자의 통제 범위를 벗어난 요인(시스템 오류, 하드웨어 문제, 펌웨어 비호환성)으로 인해 발생합니다. 목표는 크래시율을 0.1% 이하로 낮추고 남은 크래시에 대한 대응 시간을 최소화하는 것입니다.
크래시 리포터는 크래시 발생 시 스택 트레이스, 메모리 상태 및 기기 정보를 수집합니다. 분석은 사용자 행동 데이터를 수집합니다. Crashlytics는 두 접근 방식을 결합하여 사용자 지정 키와 함께 크래시 컨텍스트를 제공합니다.
ProGuard와 R8은 지적 재산권을 보호하기 위해 코드를 난독화합니다. 난독화 해제를 위해 게시 시 매핑 파일을 Crashlytics에 업로드하세요. 매핑 파일이 없으면 스택 트레이스가 실제 클래스 및 메서드 이름 대신 a.a(), b.b()를 표시합니다.
Android에서는 Thread.setDefaultUncaughtExceptionHandler를 통해: 라이브러리가 자체 핸들러를 등록하여 처리되지 않은 예외를 먼저 받고, 데이터를 저장한 후에 프로세스를 종료합니다. iOS에서는 NSException에 NSSetUncaughtExceptionHandler를, 신호에 Mach exception handler를 사용합니다.
Fatal — 애플리케이션이 종료되었습니다. Non-fatal(잡힌 예외) — 개발자가 try-catch로 예외를 잡았지만 잠재적인 문제를 나타낼 수 있습니다. Crashlytics는 이러한 유형을 구분하고 대시보드를 어지럽히지 않도록 non-fatal을 별도로 필터링할 수 있습니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.