
사라진 결제의 행방을 추적하라
사건은 이러하다.
그날도 평소처럼 평화로운 오후였다.
사용자는 결제 화면에서 신중하게 결제 수단을 고르고, 한껏 기대에 부풀어 ‘결제하기’ 버튼을 눌렀다.
이제 곧 결제가 완료되고, 상품권이 발송될 터였다.
그런데, 그때였다.
은행 앱으로 연결된 순간, 우리의 앱은 잠시 무대 뒤로 퇴장했다.
잠시면 돌아올 줄 알았다. 하지만 그것은 그의 마지막 기억이었다.
사용자는 은행 앱에서 오랜 시간 머물렀다.
카드를 선택하고, 비밀번호를 입력하고, 그러나 마지막 순간에 결제를 취소했다.
그리고 돌아왔을 때, 앱은 여전히 그 자리에 있었다.
하지만 무언가 달랐다.
화면은 익숙했지만, 기억은 사라져 있었다.
결제 금액도, 선택한 선물 정보도, 입력했던 메시지도 모두 흔적이 없었다.
마치 새로 설치한 앱처럼, 모든 것이 처음부터였다.
그의 화면에는 아무것도 남지 않았다.
결제는 하지도 못했고, 지금껏 입력해온 모든 정보까지 잃어버렸다.
나는 한동안 멍하니 로그를 바라봤다.
‘분명 아무 문제 없었는데... 도대체 왜?’
그 순간, 나는 확신했다.
이건 단순한 ‘버그’가 아니다.
프로세스 실종 사건이다.
첫 번째 수사 - 프로세스는 왜 사라졌는가?
사건 현장은 너무도 깨끗했다.
마치 누군가 일부러 흔적을 지운 듯했다.
로그조차 남지 않은 화면, 앱은 조용히 사라졌고, 스택에는 그 어떤 기록도 없었다.
나는 잠시 숨을 고르고, 눈앞의 정적을 바라봤다.
“이건 단순한 크래시가 아니다.”
이상할 만큼 조용했다.
예외도, 로그캣도, 트레이스도 남지 않았다.
마치 앱이 스스로 조용히 퇴장한 듯했다.
“혹시... 프로세스가 메모리에서 제거된 건 아닐까?”
직감이었다.
그 순간, 머릿속에서 수많은 가능성이 스쳐 지나갔다.
앱이 메모리에서 제거되는 이유는 다양하다.
너무 오래 다른 앱(은행 앱)에 머물렀거나,
백그라운드에서 시스템이 메모리를 회수했거나,
혹은 내부 어딘가에서 메모리릭이 누적되어
스스로를 조금씩 갉아먹은 것일 수도 있다.
하지만 이건 어디까지나 추측이었다.
우리에게 필요한 건, 증거다.
나는 재현에 돌입했다.
긴 체류, 화면 전환, 메모리 압박.
은행 앱으로 넘어갔다가 되돌아오기,
다른 앱을 여러 번 실행하고 복귀하기,
시스템 자원을 극한까지 몰아붙이기.
그리고, 몇 차례의 시도 끝에 마침내 그 순간이 찾아왔다.
앱 프로세스가 메모리에서 제거되는 순간이었다.
화면은 고요했지만, 내부에서는 모든 Activity 스택이 사라졌다.
ViewModel은 더 이상 살아 있지 않았고, Repository는 초기화되었으며,
마지막 남은 데이터는 한 줌의 메모리 흔적만 남기고 증발했다.
그제야 사건의 첫 퍼즐이 맞춰졌다.
범인은 바로, 시스템의 ‘프로세스 킬’이었다.
두 번째 수사 - 복구되지 않은 기억의 이유
프로세스가 사라졌다는 사실은 분명했다.
하지만 사건은 거기서 끝나지 않았다.
죽음은 언제나 복구의 시작이어야 한다.
앱이 다시 실행되고, 사용자가 다시 돌아왔다면 무언가 남아 있어야 했다.
결제 정보든, 상품권 정보든, 최소한 “여기까지 했었다”는 흔적이라도.
그러나 돌아온 화면은 텅 비어 있었다.
모든 데이터는 사라지고, 앱은 마치 처음 보는 사람처럼 나를 맞이했다.
나는 복구되지 않은 이유를 차분히 정리했다.
상태는 저장되지 않았고, 데이터는 기억되지 않았으며, 복원할 단서조차 존재하지 않았다.
프로세스가 죽는 건 시스템의 일이라 해도, 그 이후의 복구는 개발자의 책임이었다.
그 공백을 메우지 못한다면, 사용자는 매번 처음부터 다시 시작해야 한다.
앱에게 있어 죽음은 피할 수 없지만, 망각은 막을 수 있다.
이제 사건의 본질은 드러났다.
범인은 시스템이었고, 방조자는 복구 로직의 부재였다.
다음 수사에서는, 이 사라진 기억을 되살리기 위한
SavedStateHandle, Repository 캐싱, Activity Bundle 저장에 대해서 본격적으로 알아보자.
세 번째 수사 - 사라진 기억을 되살리는 방법
사건의 전모는 드러났다.
프로세스는 시스템에 의해 사라졌고,
복구는 준비되지 않았다.
이제 남은 건 단 하나, 사라진 기억을 되살리는 기술이었다.
나는 먼저, 살아남은 기록들을 검토했다.
로그의 틈새에서 희미하게 남은 흔적들.
onSaveInstanceState()가 호출되지 않은 순간,
메모리에서 증발한 ViewModel의 상태.
결국 결론은 하나였다.
“죽음을 막을 수 없다면, 복구를 설계해야 한다.”
복구는 기적이 아니다.
정확한 구조, 명확한 책임 분리,
그리고 데이터를 어디에, 어떻게 남겨둘 것인가에 달려 있었다.
1. onSavedStateHandle — 마지막 유언을 남기는 방법
ViewModel이 살아 있는 동안, UI 상태를 실시간으로 저장해둘 수 있다.
프로세스가 사라지더라도, 시스템이 저장한 상태는 다음 실행 시 그대로 복원된다.
SavedStateHandle은 앱의 단기 기억이다.
잠시 백그라운드로 나갔다 돌아와도, 사용자가 마지막으로 머물던 위치와 입력값을 기억한다.
class PaymentViewModel(
savedStateHandle: SavedStateHandle
) : ViewModel() {
var inputText = savedStateHandle.getStateFlow("inputText", "")
fun onTextChanged(value: String) {
savedStateHandle["inputText"] = value
}
}
비록 짧은 기억이지만, 이것만으로도 사용자는 “앱이 나를 기억하고 있다”는 신뢰를 느낀다.
2. Repository 캐싱 - 데이터의 은신처를 만들어라
하지만 모든 상태를 메모리에만 의존할 순 없다.
ViewModel은 죽으면 기억도 함께 사라진다.
따라서 Repository 레이어에서 중요한 데이터는 미리 캐싱해야 한다.
Room, SharedPreferences, DataStore
이름은 달라도 역할은 같다.
데이터를 잠시 숨겨두는 은신처다.
앱이 다시 살아날 때, Repository는 “이전에 봤던 기억”을 복원시킨다.
사용자는 아무 일 없었던 것처럼 결제를 이어간다.
3. Activity Bundle - 마지막 보호막
모든 복구 시나리오의 최후 보루는 Activity의 Bundle이다.
onSaveInstanceState()에 꼭 필요한 데이터를 담아두면,
Activity가 재생성될 때 자동으로 복원된다.
이건 일종의 보험이다.
“혹시 모를 사고에 대비해 남겨두는 복구 파일.”
override fun onSaveInstanceState(outState: Bundle) {
outState.putString("paymentId", paymentId)
super.onSaveInstanceState(outState)
}
짧은 코드 한 줄이지만,
그 한 줄이 사용자 경험 전체를 지켜낸다.
수사 종결 보고서 - 프로세스의 죽음이 남긴 것
나는 이 세 가지 방법을 종합해 결론을 내렸다.
복구의 기술은 곧 신뢰의 기술이다.
앱은 언제든 죽을 수 있다.
하지만 사용자는 그 죽음을 알아차려서는 안 된다.
자연스럽게 이어지는 경험, 그것이 복구의 완성이다.
그날의 사건 이후, 나는 로그창이 아니라 사용자의 여정을 보기 시작했다.
이제 더 이상 프로세스의 죽음은 두렵지 않다.
왜냐하면, 나는 그 죽음을 디자인할 줄 알게 되었기 때문이다.
“프로세스의 실종을 막을 수는 없다.
하지만 사용자가 그 실종을 느끼지 않게 할 수는 있다.
그것이 바로, 개발자가 만들 수 있는 최고의 경험이다.”
- 개발탐정 코난의 사건노트 중에서 -
'Develop > Kotlin' 카테고리의 다른 글
| 안드로이드 신입 개발자의 시선에서 본 테스트 코드 (0) | 2025.12.28 |
|---|---|
| Jetpack Compose 환경에서 ExoPlayer 최적화 (0) | 2025.08.15 |
| 안드로이드에서 일회성 이벤트 처리, 어떻게 할 것인가? (2) | 2024.10.27 |
| 너는 왜 inline Composable이야? (1) | 2024.10.22 |
| Data Binding 프로퍼티 실종 사건 수사 일지 (37) | 2024.05.12 |