Global Exception Handler: 본질, 작동 원리 및 프로젝트 구현

저자: IT Sectr 게시일: 2026-05-27 읽는 시간: 8 분

Global Exception Handler — 처리되지 않은 예외를 중앙에서 포착하여 모바일 앱 충돌을 방지하는 메커니즘입니다. Apple Developer, 2024에 따르면, 적절한 예외 처리는 충돌 발생 횟수를 40–60% 줄이고 사용자 경험을 향상시킵니다. 이러한 처리기가 없으면 백그라운드 스레드에서 처리되지 않은 예외가 발생할 때 앱이 즉시 종료됩니다.

핵심 사항

  • Global Exception Handler — 앱의 모든 처리되지 않은 예외를 수집하는 중앙 지점, 충돌 방지
  • iOS NSSetUncaughtExceptionHandler — Apple 플랫폼에서 Objective-C 예외를 인터셉트하는 C 함수
  • Android Thread.setDefaultUncaughtExceptionHandler — 전역 예외 인터셉트를 위한 플랫폼 내장 메커니즘
  • 종료 전 로깅 — 처리기의 주요 작업: 프로세스 종료 전 충돌 정보 저장
  • 우아한 저하 — 처리기를 통해 충돌 대신 사용자에게 적절한 오류 화면 표시 가능

Global Exception Handler란?

Global Exception Handler — 앱의 개별 함수나 모듈 수준에서 처리되지 않은 예외를 포착하는 중앙 집중식 메커니즘입니다. 모바일 개발 맥락에서 이러한 처리기는 프로세스의 비정상 종료 전 마지막 방어선 역할을 합니다.

iOS와 Android는 전역 처리기를 설정하기 위한 내장 API를 제공합니다. Apple은 Objective-C 환경을 위해 NSSetUncaughtExceptionHandler를 사용하고, Google은 Java/Kotlin에서 Thread.setDefaultUncaughtExceptionHandler를 제공합니다. 두 메커니즘 모두 앱의 모든 스레드에서 try-catch 구조로 포착되지 않은 예외를 캡처합니다.

Crashlytics(Google, 2024)에 따르면, 약 25%의 충돌이 백그라운드 스레드의 처리되지 않은 예외로 인해 발생합니다 — Global Exception Handler가 특히 중요한 영역입니다. 개발자들은 종종 UI 스레드에 집중하고 비동기 작업을 잊어버립니다.

전역 처리기를 사용한다고 해서 로컬 오류 처리가 대체되는 것은 아니며 보완됩니다. 주요 작업은 예외 발생 시 앱 상태에 대한 최대 정보를 저장하고 올바르게 종료하는 것입니다.

전역 예외 처리기의 작동 방식

작동 메커니즘 Global Exception Handler는 운영 체제 신호 또는 런타임 예외를 인터셉트하는 데 기반합니다. 코드가 try-catch 블록으로 포착되지 않은 예외를 던지면 제어가 미리 등록된 처리기로 전달됩니다.

iOS에서는 처리기가 NSSetUncaughtExceptionHandler를 통해 등록되고 전체 스택 추적이 포함된 NSException 객체를 받습니다. Android에서는 Thread와 Throwable을 허용하는 Thread.setDefaultUncaughtExceptionHandler가 사용됩니다 — 예외 유형, 메시지 및 호출 스택에 대한 액세스를 제공합니다.

충돌 데이터를 수신한 후 처리기는 세 가지 필수 작업을 수행합니다: 로컬 저장소에 로그 쓰기, Crashlytics 또는 Sentry에 보고서 보내기, 앱을 올바르게 종료하기. Apple WWDC 2023에 따르면 처리기의 실행 시간은 5초로 제한됩니다 — 이후 시스템이 프로세스를 강제 종료합니다.

iOS 13부터 Swift 애플리케이션을 위해 Signals API가 도입되어 예외뿐만 아니라 운영 체제 신호(SIGABRT, SIGSEGV, SIGBUS)도 처리하여 처리기의 적용 범위를 저수준 메모리 오류까지 확장합니다.

iOS에서 Global Exception Handler 구현

구현 iOS에서 전역 처리기를 설정하려면 NSSetUncaughtExceptionHandler를 통해 C 함수를 설정해야 합니다. 처리기는 처리되지 않은 예외 발생 시 동기적으로 호출되며 전체 오류 컨텍스트를 받습니다.

objective-c
void handleUncaughtException(NSException exception) {
    NSDictionary userInfo = [exception userInfo];
    NSArray stackTrace = [exception callStackSymbols];
    NSString reason = [exception reason];

    // 크래시 로그를 로컬 파일에 저장
    NSString logPath = [NSSearchPathForDirectoriesInDomains(
        NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
    [exceptionLog writeToFile:logPath atomically:YES];
}

int main(int argc, char argv[]) {
    NSSetUncaughtExceptionHandler(&handleUncaughtException);
    return UIApplicationMain(argc, argv, nil, nil);
}

iOS 구현의 중요한 특징: 처리기는 Objective-C 예외만 포착합니다. throw-catch 메커니즘을 사용하는 Swift 오류는 이 처리기에 도달하지 않습니다 — Swift Error Handling을 통한 별도 처리가 필요합니다. iOS 14부터 Apple은 최대 범위를 위해 NSSetUncaughtExceptionHandlerSignals API를 결합할 것을 권장합니다.

Apple Technical Note TN2151에 따르면, 처리기 호출 후 앱은 5초 이내에 종료되어야 합니다. 처리기에서 반환된 후 실행을 계속하려는 모든 시도는 정의되지 않은 동작과 추가 충돌을 초래합니다.

Android에서 Global Exception Handler 구현

AndroidThread.setDefaultUncaughtExceptionHandler를 통해 더 유연한 전역 예외 처리 메커니즘을 제공합니다. 처리기는 예외가 발생한 스레드에 대한 참조와 Throwable 객체 자체를 받습니다.

kotlin
class GlobalExceptionHandler : Thread.UncaughtExceptionHandler {

    override fun uncaughtException(thread: Thread, throwable: Throwable) {
        // 크래시 로그를 파일에 저장
        val stackTrace = throwable.stackTraceToString()
        val crashLog = "CRASH: ${thread.name}\n${stackTrace}"

        val file = File(context.cacheDir, "crash_log.txt")
        file.writeText(crashLog)

        // Crashlytics로 전송
        FirebaseCrashlytics.getInstance()
            .recordException(throwable)

        // 프로세스 종료
        android.os.Process.killProcess(
            android.os.Process.myPid()
        )
    }
}

// Application.onCreate에서 설정
class App : Application() {
    override fun onCreate() {
        super.onCreate()
        Thread.setDefaultUncaughtExceptionHandler(
            GlobalExceptionHandler()
        )
    }
}

Android 구현의 주요 차이점: 각 스레드에는 자체 처리기가 있으며, setDefaultUncaughtExceptionHandler는 개별 처리기가 할당되지 않은 모든 스레드에 대해 처리기를 설정합니다. 이는 전역 범위를 보장합니다 — UI 스레드에서 백그라운드 AsyncTask 및 코루틴까지.

Android 12+에는 제한 사항이 있습니다: uncaughtException 호출 후 앱은 100밀리초 이내에 종료되어야 합니다. 처리기가 장기 실행 작업을 수행하면 로그가 기록되기 전에 시스템이 프로세스를 종료할 수 있습니다. 충돌 보고서를 보내려면 백그라운드 서비스를 사용하는 것이 좋습니다.

Global Exception Handler 모범 사례

첫 번째 규칙 — 처리되지 않은 예외 후 앱 작동을 복원하려고 시도하지 마십시오. 충돌 후 앱 상태는 정의되지 않으며, 실행을 계속하면 사용자 데이터가 손상될 수 있습니다.

처리기 실행 시간 최소화

시간 제한 — Global Exception Handler의 주요 기술적 제약입니다. iOS에서는 5초, Android에서는 100밀리초입니다. 처리기 내에서는 예외 유형, 호출 스택 및 몇 가지 주요 변수의 상태 등 최소 데이터 세트만 저장해야 합니다.

네트워크 요청 전송, 데이터베이스 쓰기 및 복잡한 직렬화는 지연 메커니즘으로 연기되어야 합니다 — 예를 들어 로그를 파일에 저장하고 다음 앱 실행 시 보냅니다.

충돌 보고 시스템과의 결합

충돌 보고 서비스 — Firebase Crashlytics, Sentry, Bugsnag — 자체 전역 처리기를 설정합니다. 개발자가 그 위에 사용자 정의 처리기를 설정하는 경우, 자신의 작업 후 충돌 보고 시스템에 제어권을 넘겨야 합니다. Android에서는 처리기 구성이 사용됩니다: 자체 로직을 실행한 다음 이전 처리기를 호출합니다.

Firebase Crashlytics의 경우 사용자 정의 Thread.setDefaultUncaughtExceptionHandler를 설정하지 않는 것이 좋습니다 — Crashlytics SDK가 초기화 시 자동으로 수행합니다.

추가 정보 로깅

사용자 컨텍스트 — 표준 호출 스택 외에도 앱 버전, OS 버전, 사용 가능한 메모리 크기 및 충돌 전 가동 시간을 로깅하는 것이 유용합니다. 이 데이터는 문제 재현 및 해결에 매우 중요합니다.

iOS에서는 NSSetUncaughtExceptionHandler를 쓰기뿐만 아니라 synchronize 플래그와 함께 NSUserDefaults에 임시 데이터 저장에도 사용할 수 있습니다 — 즉시 프로세스 종료 시에도 지속성을 보장합니다.

출시 전 처리기 테스트

필수 테스트 — Global Exception Handler는 CI/CD의 모든 단계에서 테스트되어야 합니다. iOS에서는 @throw NSException을 통해 테스트 예외를 트리거할 수 있고, Android에서는 throw RuntimeException()을 통해 트리거할 수 있습니다. 처리기가 호출되고, 로그가 저장되며, 앱이 올바르게 종료되는지 확인합니다.

Google I/O 2023에 따르면, 프로덕션에서 30% 이상의 충돌이 개발자가 테스트하지 않은 기기에서 발생합니다 — 다양한 Android 버전, 사용자 지정 펌웨어, 제한된 메모리.

처리기 사용 시 일반적인 실수

첫 번째이자 가장 흔한 실수 — 예외 처리 후 앱 실행을 계속하려는 시도. uncaughtException 호출 후 앱은 불안정한 상태에 있으며, 추가 작업은 연쇄 오류와 데이터 손상을 일으킬 수 있습니다.

두 번째 실수 — 처리기 내에서 장기 실행 작업 수행. 네트워크 요청, 대용량 파일 쓰기 또는 복잡한 계산은 프로세스 강제 종료 전에 완료되지 않습니다. Apple Technical Q&A QA1468에 따르면, 처리기 내에서 HTTP 요청을 보내려는 시도는 충돌 보고서 손실의 주요 원인입니다.

세 번째 실수 — 백그라운드 스레드 무시. 메인 스레드에만 설정된 Global Exception Handler는 코루틴, DispatchQueue, AsyncTask 또는 RxJava의 충돌로부터 보호하지 않습니다. Android에서는 각 스레드에 자체 처리기가 있어야 하며, setDefaultUncaughtExceptionHandler는 개별 처리기가 없는 스레드에 대해서만 이 문제를 해결합니다.

네 번째 실수 — OS 신호에 대한 폴백 부재. iOS의 NSSetUncaughtExceptionHandler는 SIGABRT, SIGSEGV 및 SIGBUS를 포착하지 않습니다. 이러한 신호는 sigaction API를 통한 별도 처리기 설정이 필요합니다. 개발자는 앱이 단 하나의 충돌 보고서도 없이 충돌할 때 이를 알게 됩니다.

다섯 번째 실수 — 기밀 데이터 로깅. 충돌 로그에 이메일, 인증 토큰 또는 사용자의 개인 데이터가 포함될 수 있습니다. 이는 GDPRApple App Store Review Guidelines을 위반합니다. 정규식 또는 허용된 필드의 화이트리스트를 통해 전송되는 데이터를 항상 필터링하십시오.

자주 묻는 질문

전역 예외 후 앱을 복원할 수 있나요?

아니요 — Global Exception Handler가 호출된 후 앱 상태는 정의되지 않습니다. 실행을 계속하려는 모든 시도는 데이터 손상을 초래할 수 있습니다. 유일한 올바른 조치는 충돌 로그를 저장하고 프로세스를 종료하는 것입니다.

Global Exception Handler는 모든 유형의 오류를 포착하나요?

전부는 아닙니다 — iOS에서 NSSetUncaughtExceptionHandler는 Objective-C 예외만 포착합니다. Swift 오류 및 OS 신호(SIGSEGV, SIGABRT)는 별도 처리기가 필요합니다. Android에서 Thread.setDefaultUncaughtExceptionHandler는 모든 RuntimeException을 포착하지만 JNI를 통한 네이티브 코드 오류는 포착하지 않습니다.

내 처리기 후 충돌 보고 시스템에 제어를 전달하려면?

자체 처리기를 설정하기 전에 Thread.getDefaultUncaughtExceptionHandler()를 통해 이전 처리기에 대한 참조를 저장합니다. 처리기 끝에서 previousHandler.uncaughtException(thread, throwable)을 호출합니다 — 이렇게 하면 Crashlytics 또는 Sentry가 데이터를 수신할 수 있습니다.

C/C++ 네이티브 코드에서 충돌이 발생하면 어떻게 하나요?

네이티브 코드의 경우 sigaction()을 통한 신호 처리가 필요합니다 — SIGSEGV, SIGABRT, SIGBUS. Android에서는 Google Breakpad 또는 Crashpad를 사용할 수 있습니다. iOS 버전 13부터 mach 예외 처리를 위한 Signals API를 사용할 수 있습니다.

Global Exception Handler가 성능에 영향을 미칠 수 있나요?

아니요 — 처리기 설정은 예외 발생 시에만 영향을 미칩니다. 정상 앱 작동 시 오버헤드는 없습니다. 유일한 위험은 처리기가 Activity 또는 Context에 대한 참조를 보유하여 가비지 컬렉션을 방해하는 메모리 누수입니다.

요약

  • Global Exception Handler — 충돌 전 마지막 방어선, 모든 프로덕션 앱에서 필수
  • iOS NSSetUncaughtExceptionHandler는 5초 처리 제한으로 Objective-C 예외 포착
  • Android Thread.setDefaultUncaughtExceptionHandler는 개인 처리기가 없는 모든 스레드에서 작동
  • 처리기 실행 시간은 최소화해야 함 — 데이터 저장 후 복구 시도 없이 프로세스 종료
  • OS 신호(SIGSEGV, SIGABRT)는 표준 처리기로 포착되지 않음 — sigaction API 필요
  • 충돌 보고 시스템은 처리기 합성을 통해 호출, 자체 로직 후 제어권 전달
  • CI/CD에서 처리기 테스트 — 프로덕션에서 충돌 보고서 손실 방지를 위한 필수 단계

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기