개요

최근 회사에서 업무를 진행하다, 앱이 비정상적으로 종료되는 시점에 API 요청을 해야하는 요구 사항을 받았습니다. 이에 대한 접근 방법과 해결책을 공유합니다.

우선, 앱이 종료되는 시점을 이해하려면 iOS의 앱 생명 주기를 이해해야합니다. 공식 문서에 따르면, 앱의 생명 주기는 앱이 실행, 활성화, 비활성화, 백그라운드, 종료 등의 상태를 거치는 동안 시스템이 이를 관리하고 앱이 각 상태 변화에 대응하도록 하는 과정을 말합니다.

이러한 상태는 AppDelegate이라는 싱글톤 객체가 관리하며, 다양한 상태 변화를 콜백 형태로 수신받을 수 있습니다.


앱이 종료되는 시점

앱의 프로세스가 종료된 시점에 대한 이벤트는 func applicationWillTerminate(UIApplication) 메서드가 담당하며, 해당 메서드가 리턴되면 앱은 완전히 종료 되었다고 볼 수 있습니다. 또한, 이 시점에 willTerminateNotification를 통해 notification를 보냅니다.


1. Rx의 willTerminate를 사용한 접근

rx의 willTerminate 옵저버블은 앱 프로세스가 종료되었다는 이벤트를 방출합니다. 처음엔 이를 통해, API 요청하는 코드를 작성했습니다.

// ViewModel...
let appTerminate = UIApplication.rx.willTerminate.asObservable()

// Observable<Event<T>> 타입
let request = appTerminate
    .map { 
        //API 요청..
    }
    .share()

// success Observable
let success = request
    .compactMap(\\.element)

// error Observable
let error =  request
    .compactMap(\\.error)
    
.
.
.
// ViewController
output.success
    .subscribe(with: self) { _, _ in
        print("API 응답")
    }
    .disposed(by: disposeBag)

output.error
    .subscribe(with: self) { _, _ in
        print("API error")
    }
    .disposed(by: disposeBag)

하지만, 실체 요청에 대한 응답은 전혀 오지 않았고 세부적인 로그를 통해 확인해보니 요청 자체가 들어가지 않았습니다. 그래서, willTerminate의 내부 구조를 파악해 보았습니다. 앱이 종료되기 직전이 아니라 앱이 완전히 종료되었다는 콜백을 주고 있었고, 이를 구독하더라도 당연히 앱 프로세스가 이미 종료되었기 때문에 작업을 실행하고 응답을 받을 수 있는 상태가 아니었습니다.

/// Reactive wrapper for `UIApplication.willTerminateNotification`
public static var willTerminate: ControlEvent<Void> {
    let source = NotificationCenter.default.rx.notification(UIApplication.willTerminateNotification).map { _ in }
    
    return ControlEvent(events: source)
}

따라서, 앱이 종료되기 직전의 상태를 찾아야했습니다.


2. AppDelegate의 applicationWillTerminate(UIApplication)를 이용한 접근

앞서 개요에 applicationWillTerminate 메서드가 ‘리턴되는 시점’에 앱이 완전히 종료된다는 것을 설명했습니다. 그러면, ‘메서드 내부에 API를 요청하는 코드를 작성하면, 되지 않을까?’ 라는 생각이 들었습니다. 하지만, 이번에도 역시나 실패했습니다. 그 이유는 API 요청은 async하게 관리되기 때문에, 메인 스레드에서 동작하는 AppDelegate에서는 이를 제어할 수 없었습니다.

쉽게 말해, applicationWillTerminate 메서드 내부에서 API 요청을 하더라도 API 요청의 응답과 무관하게, applicationWillTerminate는 리턴될 수 있다는 것입니다. 이때 하나의 트릭을 사용할 수 있습니다. 바로 sleep 메서드를 통해, 메인스레드에 일정 시간 lock을 거는 것입니다.

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    var disposeBag = DisposeBag()    
    //applicationWillTerminate 메서드 호출을 알려주는 트리거
    private let startTerminateApp = PublishSubject<Void>() 
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        bind() // 바인딩
        
        return true
    }
    
    // 앱 종료 이벤트 수신
    func applicationWillTerminate(_ application: UIApplication) {
        startTerminateApp.onNext(())
        sleep(5) // sleep을 통해 스레드에 lock을 걸어, return 지연
    }
    
    func bind() {
        let startTerminateApp = startTerminateApp.asObservable()
        
        // Observable<Event<T>> 타입
        let request = startTerminateApp
            .map { 
                //API 요청..
            }
            .share()
        
        // success Observable
        let success = appTerminate
            .compactMap(\\.element)
        
        // error Observable
        let error =  appTerminate
            .compactMap(\\.error)

        // ViewController
        success
            .subscribe(with: self) { _, _ in
                print("API 응답")
            }
            .disposed(by: disposeBag)
        
        error
            .subscribe(with: self) { _, _ in
                print("API error")
            }
            .disposed(by: disposeBag)
    }
    .
    .
    .
}

코드를 작성한 과정은 아래와 같습니다.