[iOS] 앱이 종료되는 시점에 API 요청하기

lyoodong's avatar
Aug 06, 2024
[iOS] 앱이 종료되는 시점에 API 요청하기

개요

최근 회사에서 업무를 진행하다, 앱이 비정상적으로 종료되는 시점에 API 요청을 해야하는 요구 사항을 받았습니다. 이에 대한 접근 방법과 해결책을 공유합니다.
우선, 앱이 종료되는 시점을 이해하려면 iOS의 앱 생명 주기를 이해해야합니다. 공식 문서에 따르면, 앱의 생명 주기는 앱이 실행, 활성화, 비활성화, 백그라운드, 종료 등의 상태를 거치는 동안 시스템이 이를 관리하고 앱이 각 상태 변화에 대응하도록 하는 과정을 말합니다.
이러한 상태는 AppDelegate이라는 싱글톤 객체가 관리하며, 다양한 상태 변화를 콜백 형태로 수신받을 수 있습니다.

앱이 종료되는 시점

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

1. Rx의 willTerminate를 사용한 접근

앱을 관리하는 UIApplication은 앱이 종료되는 시점에 이벤트를 방출하는 willTerminate 옵저버블을 제공합니다. 처음엔 단순하게 이를 통해, API 요청하는 코드를 작성했습니다.
// ViewModel... let appTerminate = UIApplication.rx.willTerminate.asObservable() // Observable<Event<T>> 타입 let request = appTerminate .map { //API 요청.. } .share() // success Observable let success = appTerminate .compactMap(\.element) // error Observable let error = appTerminate .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의 내부 구조를 파악해 보았습니다. 역시나 willTerminateNotification의 콜백을 통해 이벤트를 받고 있었습니다. 즉, 앱이 종료되기 직전이 아니라 앱이 완전히 종료되었다는 콜백을 받고 있었고 이를 구독하더라도 당연히 메모리 리소스가 없기 때문에, API 요청 코드는 실행될 수 없습니다.
/// 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) } . . . }
코드를 작성한 과정은 아래와 같습니다.
  1. applicationWillTerminate 메서드를 호출되었음을 알려주는 트리거 startTerminateApp 선언
  1. API 요청하는 bind 메서드 작성 후, application(app의 launch가 완료됨을 알리는) 메서드에서 호출
  1. applicationWillTerminate에서 트리거에 .onNext(())를 전달
  1. applicationWillTerminate에서 sleep을 통해, return 지연
 
이렇게 하면, applicationWillTerminate은 메인 스레드에서 실행되기 때문에 API 요청과 같이 백그라운드 스레드에서의 실행은 이와 무관하게 수행될 수 있고, 결과적으로 5초 이내에만 API 요청이 이루어진다면 의도했던 동작을 구현할 수 있게됩니다.
이러한 의도적 지연은 애플의 공식 문서에서도 확인할 수 있습니다. applicationWillTerminate가 호출된 후 5초 이내의 지연은 허용되며, 5초를 초과하여 리턴되지 않았을 경우 경우, 시스템 OS에서 프로세스를 중단할 수 있다고 말합니다.
💡
Discussion
This method lets your app know that it is about to be terminated and purged from memory entirely. You should use this method to perform any final clean-up tasks for your app, such as freeing shared resources, saving user data, and invalidating timers. Your implementation of this method has approximately five seconds to perform any tasks and return. If the method does not return before time expires, the system may terminate the process altogether. .
.

한계점

다만, 5초의 시간이 주어지더라도 사용자의 네트워크 불안정성 혹은 원인 모를 이유로 API 요청 작업이 지연될 경우에는 그 한계를 가질 수 밖에 없는 방법입니다. 또한, 5초 라는 시간적 제약을 가지기 때문에 해당 메서드 내부에서 가능한 작업량을 줄여야 한다는 것을 알 수 있습니다.
Share article
RSSPowered by inblog