때때로 앱은 크레쉬가 난다. 크레쉬는 사용자의 워크플로우를 방해하는데, 데이터가 손실되거나 백그라운드에서 앱 오퍼레이션이 어떠한 손상을 주기도 한다. 개발자에게는 해결하기 어려운 크레쉬는 다시 재현하기 힘들거나 찾아내기 힘든 것들일 것이다.

나는 최근에 Castro에서 발견하기 힘든 많은 크레쉬들 때문에 생긴 버그를 찾아다가 고쳤는데, 이 이야기를 공유하면서, 여러분이 경험하는 비슷한 이슈에대해 도움이 되길 바란다.

Oisin과 나는 9월에 Castro2.1을 배포했다. 그리고 곧 아이튠즈 커넥트를 통해 Castro 크레쉬가 높게 치솟는다는 보고를 받았다.

2.1버전 이후 Crastro2 크레쉬 리포트를 보여주는 차트2.1버전 이후 Crastro2 크레쉬 리포트를 보여주는 차트


아이튠즈 커넥트 크레쉬 리포트
흥미롭게도 이 크레쉬들은 우리가 주로 쓰는 크레쉬 리포팅 서비스(HockeyApp)에 나타나지 않았다. 따라서 실제로 그 문제를 발견하는데까지 시간이 좀 걸렸다. 또한 모든 크레쉬를 알아내기 위해 개발자들은 아이튜즈 커넥트나 Xcode를 통해 크레쉬 리포트를 확인했다. (업데이트: "써트파티 크레쉬 리포터는 기록을 남기기위해 in-process 핸들러를 사용한다. 그러나 OS가 밖에서 여러분 앱을 죽이면 핸들러는 절대 동작하지 않을 것이다"고 Greg Parker가 알려주었다. 추가적으로 HockeyApp의 공동 창시자인 Andreas Linde가 한 아티클을 소개해주었는데, 글은 HockeyApp이 어떤 종류의 크레쉬를 찾아내고, 찾아내지 못하는지 설명해주는 글이었다.)

여러분이 만약 앱 개발자이고 여러분의 계정으로 로그인 했다면 Xcode가 애플에의해 사용자로부터 모은 크레쉬 리포트를 검토할 수 있게 해준다. 이 기능은 Organizer 윈도우의 크레쉬 탭에서 찾을 수 있다. 앱 버전을 고를 수 있고, 개발자와 정보를 교환하고자 동의했던 사용자로부터 애플이 모은 크레쉬 리포트를 다운받을 것이다.

Xcode에서 이 부분을 사용할 때 크레쉬가 나는 경향이 있다는 것을 발견했다. 특히 크레쉬 리포트에서 쓰레드 방향의 디스클로저를 열고 닫는 토글링시 크레쉬가 나타났다. 쉽게 하는 방법은 리스트 한편에 크레쉬를 오른쪽 마우스로 클릭하고, "Show in Finder"를 선택한다. 나타난 번들을 더 자세히 보면 일반 텍스트 파일로 크레쉬 리포트를 볼 수 있다.

크레쉬 조사하기
크레쉬는 원래 코드 경로에의해 발생되지만, 항상 데이터베이스 쿼리 실행 메소드에서 끝난다.

먼저 나는 스레드 이슈가 아닐까 의심했었다. 지난 몇년동안 스레드 버그러부터 나온 고통을 많이 겪었기 때문에, 나는 항상 그 고통을 먼저 떠올린다. 텍스트 파일로 된 크레쉬 리포트를 열어보니 Xcode가 보여준 것보다 훨씬 세부적인 내용을 볼 수 있었다. 예외타입은 EXC_CRASH (SIGKILL)이고, 노트(note)는 EXC_CORPSE_NOTIFY이며, 종료 원인은 Code 0xdead10cc였다. 나는 0xdead10cc가 무엇을 의미하는지 찾아보았다. 구글과 애플 개발자 포럼에는 이것에대한 이야기가 그리 많지는 않았지만 Technical Note 2151에서 이야기 해주었다.

0xdead10cc 예외코드는 백그라운드에서 돌고있는 앱을 시스템 자원의 이유로 iOS에의해 종료시킨 경우를 말한다.

여기서 내가 알아낸 점은 iOS가 앱을 종료시킬때 우리 코드상에서 뭔가 실수때문만은 아니며 정책 침해 때문일 수도 있다는 것이다. 그러나 Castro는 Adreess Book database를 사용하지도 않고 시스템 리소스와 비교가능한 어떤것을 사용하지 않는다고 생각했다. 나는 앱이 백그라운드에서 너무 오래 동작해버리면 어떻게할지 고민했는데, 우리가 받는 크레쉬 리포트의 몇 경우는 고작 2초정도 동작하고 말았다.

나는 데이터베이스에서 일어나는 크레쉬를 쫒아 수많은 stack trace pointing으로부터 우리의 데이터베이스 SQLite 파일에 문제가 있음을 결국 찾아냈다. 그러나 왜 이러한 크레쉬가 2.1버전이 되서야 나타났을까?

공유된 앱 컨테이너
Castro의 2.1 릴리즈에서 최근 재생한 에피소드를 쉽게 공유하기 위한 기능인 iMessage 앱을 지원했다. 메시지 앱이 데이터베이스에 접근하는 과정에 우리는 공유된 앱 컨테이너로 데이터베이스를 옮겼다.

파일이 공유된 공간에 있으면 그 팔일이 락이 걸리는 정책을 가진다면 어떨지 고민해보았다. 아마 iOS가 앱을 멈출때 다른 프로세스가 그 파일을 사용할 수 있는지 체크하고, 만약 그렇다면 iOS는 그 앱을 종료시킨다. 이것은 무리없이 있을법한 이야기이다.

어떻게 크레쉬를 낼까
크레쉬를 고치기위해 크레쉬를 재현해보는 것은 프로그래머에게 좋은 습관이다. 그 크레쉬를 흉내내기 위해 어느 부분을 임시로 다시 짤 수 있다. 만약 크레쉬가 확실히 일어나게 해보았으면, 이런 의심을 확인해보는것과 어느정도 거리가 있으며 우리에게 이 잼제적인 문제를 테스트 할 수 있는 기회를 제공한다. 다른 대안으로는, 무턱대고 일단 고친다음 배포하여 크레쉬 리포트를 받아보는 것이다. 이떨때는 정말 이 방법밖에 없을 수도 있는데, 너무 장황한 프로세스이며 여전히 사용자에게 크레쉬를 만들고 있다.

특정 어떤 크레쉬는 다시 만들어보기 매우 어려운 경우가 있다. iOS 개발에 대한 적당한 평가가 여기에 있다고 생각한다. OS는 적극적으로 이 정책을 시행한다. 대부분의 경우 보안상이나 베터리 사용량 등에서 이 정책을 시행하면 좋은점이 많다. 그러나 이런 상황에서 테스팅이나 디버깅을 하면 더 힘들다. 정책을 살짝 바꾸고 손수 그 가능한 라이프 사이클 상태를 테스트 하는 것은 매우 번거로울 뿐더러 때론 불가능한 경우도 있다.

이번의 경우 디버거를 연결한 상태에서 앱 정지를 시키는 방법이 없음을 알아냈다. 사실 디버거는 앱 정지를 막으며 시뮬레이터는 정확하게 시뮬레이팅하지 않는다. 나는 디버거 없이 앱을 실행하고 기기의 로그를 살펴보는 방법밖에 없었다.

macOS 시에라의 새로운 콘솔 앱은 연결된 아이폰의 시스템 로그를 볼 수 있게 해준다. 기에라 이전에는 이 기능을 위해 Lemon Jar의 iOS 콘솔에 의존했으나, 애플이 로그에 접근할 수 있게 하면서 이 기술이 애플에게 받아드려지고 지원하게 되었다는 것을 알게되어 매우 기쁘다. 새로운 콘솔 앱을 어떻게 쓰는지 배우는 것도 의미있는 시간이 될 것이다. Xcode 디버거가 혼자서는 할 수 없는, 많은 iOS 동작을 보여준다. 이 로그는 시스템 전체의 로그이기때문에 디버깅에 무의미한 로그들도 많지만, 필터를 적용해서 간편하게 당신의 앱에 관련된 메시지만 뽑아낼 수 있다.

0xdead10cc 크레쉬를 찾기 위해
  • 몇백개의 데이터베이스 쿼리에 applicationDidEnterBackground 메소드를 설정한다.
  • 내 맥에서 콘솔앱을 열고 Castro를 언급한 메시지로 걸러낸다.
  • Xcode를 통해 앱을 설치하되 앱 아이콘을 눌러 앱을 실행한다.
  • 앱을 백그라운드로 보내기위해 홈버튼을 누르고 바로 포켓몬고를 열어 메모리 압박으로 인한 Castro의 정지를 기대한다.
이 단계를 몇번 거치고나면 크레쉬를 다시 만들어내어 콘솔에서 다뤄볼 수 있다. 이 흔적은 실제 크레쉬와 비슷하며 이제는 크레쉬의 원인을 찾아보기 훨씬 수훨해졌다.

그러고나서 데이터베이스에 접근할 수 있는 백그라운드 동작이 있는 앱에서 한번에 찾고 고칠 수 있게 되었다(네트워크 reachability 변화에서 앱은 백그라운드 작업 없이 리프레쉬 하였다). 리프레쉬하는 과정에서 데이터베이스에 접근할때 앱이 일시정지해 있었다면, iOS가 앱을 죽일 것이다.

백그라운드 패치를 해냄
한가지 더 놀라운 사실을 공유하고 싶다. Castro2에서는 우리의 서버가 새로운 에피소드 배포를 알려주는데, 이것이 사용자 피드의 리프레쉬를 발생시킨다. iOS가 이 메시지를 앱에 보내면 완료 블락을 가지고있는 didReceiveRemoteNotification 메소드를 호출한다. 아래는 문서에서 발췌했다.

여러분의 앱은 알림을 처리하고 특정 완료 핸들러 블락을 호출하기 위해 최대 30초까지 시간이 있다. 실제로는 알림 처리가 끝나자마자 핸들러 블락을 호출할 수 있을 것이다. 시스템은 백그라운드 경과시간, 베터리 소모량, 데이터비용을 여러분 앱에서 추적하고 있을 것이다.

이 부분에 이상한 것이 있었다. 앞에서 언급했듯 Castro는 가끔씩 2초만에 죽어버렸다. 스택 추적에서 보면 아직 완료 블락이 호출되기도 전이었다. 따라서 문서에서는 30초까지 안전하다 했어도 어쨌든 정지되어버렸다.

무슨일인지 알아보기 위해 개발자 지원 사건(developer technical support incidents)을 사용하여 꽤 놀라운 점을 발견했다. 굉장히 도움이 된 Kevin Elliott 대답을 찾았는데, 그 엔지니어의 경우가 내 경우와 비슷했다.

dead10cc 이슈가 파일 락 때문에 일어난 것이라하여
무엇이 실제로 이것을 발생시켰냐하면, 당신의 앱이 정지했을때 iOS가 당신의 앱 컨테이너에서 락이 걸린 파일(이 경우에는 SQLite 락)을 발견했기 때문이다. 그렇게 체크하는 이유는 앱 내에서 데이터가 오염되는 것을 줄이고, 관리하기 위한 방법으로 추가된 것이다. 여기서의 문재는, 락이 걸린 파일은 아직 수정중에 있고 합칠 수 없는 상태로 있을 것이다. 다른 말로는, 앱이 주어진 파일에 락을 거는 유일한 이유는 그 파일에대해 추가적으로 일련의 읽기/쓰기를 할 수도 있으며, 그 사이에 다른 쓰기가 끼어들어오지 않게 쓰기를 완료하기 위한 보장을 해주기 위함이다. 좀 더 확장하여, 파일이 아직 락이 걸려있다면 아직 쓰기를 완료하지 못했다는 의미이다. 이러한 상태의 한 파일은 몇몇 잠재적인 문제를 가진다.
  • 앱이 정지해있을때 앱을 죽이면 데이터 쓰기가 이루어질 수 없고 데이터를 오염시킨다.
  • 파일이 두 앱 간에 공유되고 두번째 앱/extension이 실행되면, 두번째 앱은 강제로 락을 풀고 일관된 상태에 파일을 복구하려 할 것이다. 첫번째 앱은 일관되지 않은 상태를 빠져나오거나, 공유된 파일을 완전히 무시한다.

백그라운드 시간의 30초에 관해서는 
... 여기서 올바른 대답은 문제를 완전히 해결하는 것이다 - 만약 델리게이션으로 여러분의 작업을 다 못끝내면 백그라운드 작업을 시작하고 앱을 정지하기 전에 iOS가 (완료 블락에서) 알려줄 것이다 ...

백그라운드에서 앱이 돌아가는 동안 공유된 컨테이너를 통해 파일을 접근할 수 있는 앱은 백그라운드 작업을 만들 수 있으며, 완료 블락이 30초동안 커버할 수 있다고 가정한다. 이런식으로 작업하기 위해 개발자들은 UIApplication에서 beginBackgroundTaskWithName:expirationHandler 메소드를 사용해 백그라운드 작업을 만들고, 백그라운드 작업이 끝나면 endBackgroundTask를 호출한다.

추가로 kevin은 한가지 더 제안을 해주었는데, 치솟는 데이터 처리가 끝났음을 보장하고 좀 더 확실하게 이상한 버그를 해결하기 위한 방법으로, 백그라운드로 넘어갈 때 앱은 데이터베이스를 닫을 것을 제안했다.

일반적으로 오퍼레이션으로 파일을 닫는 것은 불가사의한 버그(백그라운드에서 뭔가 동작하지 않아요)를 좀 더 명확한 버그(내 앱이 백그라운드에서 동작하지 않아요)로 바꿀 수 있다. 이 점이 문제를 직접 해결할 수 있게 도와줄 것이다.

이것은 꽤 스마트해보인다. 백그라운드로 갈 때 앱의 일부를 종료하는 생각이 들지 않았지만, 확실히 의미가 있다. 나는 백그라운드에서 데이터베이스를 닫는 방법을 Castro의 다음 업데이트에서 찾아볼 것이다.

결론
백그라운드에서 계속해서 동작하는 백그라운드 작업을 추가하여 우리의 베타버전에 이 이슈를 고쳤다. 우리는 곧 고친 것을 포함한 업데이트를 배포할 것이다.

아래에는 내가 배운 것들의 요약본이다.

  • 애플은 다른 서비스들이 발견하지 못한 크레쉬를 보고해준다. 아이튠즈 커넥트나 Xcode, 외부 서비스들까지도 크레쉬 리포트를 확인해보자.
  • 파일 락의 정책은 당신의 데이터베이스가 공유된 공간에 있을 때 더욱 엄격해진다.
  • 백그라운드 패치 완료 블락에 의존하는 것으로는 충분하지 않다. 활성 백그라운드 작업없이는 백그라운드에서 어떤 작업도 하지마라.
  • 앱 라이프 사이클의 특정 부분에서만 나타나는 이슈는 디버깅하기 힘들다. 아직 재현해보지 못했다면 새 시에라의 Console.app을 배워서 사용해보아라.
  • 기술 지원 사건들(Technical Support Incidents)을 기억하며, 이것도 우리가 매년 지불하는 개발자 맴버쉽에 포함된다(원문: Don’t forget about Technical Support Incidents, you get 2 included in your developer account payment every year).



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,