회사에서 앱을 만들던 중 고객사에서 특정 기사가 배송 완료 기능이 안 된다는 연락을 받았다. 문제 확인을 위해 테스트를 해봤지만 증상 재현은 되지 않았고 담당자를 통해 증상에 대한 영상과 스마트폰 기종, 안드로이드 버전 등의 정보를 요청 했다. 스마트폰 기종은 저가형 폰이였지만 그게 문제될만한 상황이라고 생각하진 않았고 안드로이드 버전도 문제가 없었다.
증상이 발생 하는 영상을 확인 해보니 배송 완료 버튼 클릭 -> 인수자 선택 -> 카메라 앱으로 이동 후 사진 촬영 후 확인 -> 앱으로 돌아와서 서버로 전송을 하는데 사진 촬영 후 앱으로 돌아 오는 경우에 문제가 발생 하는 걸로 보였다.
영상을 보고 처음 든 생각은 화면 회전으로 인한 Config Change 문제가 아닐까 생각 했다. 앱에서는 Manifast에 android:screenOrientation="portrait" 코드를 넣어 세로 고정을 하기도 했고 앱 코드에서도 requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT 코드를 사용 하여 세로 고정을 강제로 하였기 때문에 화면이 회전 되는 경우는 없었지만 카메라 앱에서는 세로 고정이 되지 않으니 문제가 발생할 수 있지 않을까 싶어서 android:configChanges="orientation" 코드를 넣어서 증상이 발생 되는 기사분에게 테스트 요청을 드렸다. 하지만 문제는 여전히 발생 했고 화면 회전이 아닌 다른 config Change의 문제일 수도 있으니 configChanges 관련 문서를 찾아 발생할 수 있는 모든 config Change를 발생 하지 않도록 Manifest에 코드를 넣어서 테스트 요청을 드렸다.
하지만 같은 문제가 또 발생 했고 직접 테스트를 하는 경우에는 증상 재현이 안 되기 때문에 원인을 찾기 힘들었다. 고객사에서 이러한 상황을 듣고 기사분에게 요청하여 문제가 발생 되는 폰으로 테스트가 가능하게끔 대여 해주셨고 본격적으로 테스트에 들어갔다.
영상에서 확인 했던대로 사진을 찍고 앱으로 돌아오면 앱 화면이 다시 그려지는듯 했다. 혹시 놓친 config change가 있는지 어떤 라이프사이클을 타는지 확인 하기 위해 onConfigurationChanged 와 각 라이프사이클에 로그를 찍어봤는데 카메라 앱에 진입할 때 onPause 까지 실행 되고 다시 돌아올 때 onConfigurationChanged도 호출 되지 않고 onDestroy 없이 바로 onCreate가 실행 되는 것이였다.
config change 문제였다면 onDestroy는 호출이 됐어야 했는데 다른 문제구나 생각이 들었고 다시 새로 정보를 찾아보던 중 Low memory kill에 대한 정보를 보게 됐다. Low memory kill 이란 Android 시스템에서 메모리가 부족하면 앱 상태에 따라 우선 순위를 매기고 우선 순위가 높은 앱의 메모리 확보를 위해 우선 순위가 낮은 앱들을 kill 한다.
LMK 우선순위
1. 시스템(PackageManager, ActivityManager 처럼 시스템 자체 프로세스)
2. 시스템 앱 (단말 제조사에서 선탑재한 앱중 AndroidManifest.xml 파일 내 의 android:persistent 속성이 true로 설정한 앱이다. 예를 들어 전화, 메시지, 카메라 앱 등이다.)
3. 가장 앞에 보이는 앱 (현재 실행중인 앱이다.)
4. 뒤에 보이는 앱 (현재 실행한 앱이 화면 전체를 가리지 않고 뒤에 보이는 앱이다)
5. Forground Service로 사용자에게 지각되는 서비스 앱.(음악재생처럼 사용자에게 보여지지 않아도 인지되는 서비스이다.)
6. 서비스 앱(일반 서비스이다.)
7. Launcher 앱(Launcher는 홈키를 통해 빈번하게 사용자에게 보여지는데 만약 죽게 되면 홈키를 누른 이후 런처가 보일때까지의 반응이 느리다.)
8. 완전히 가려진 앱(다른 앱의 Activity에 의해 완전히 가려진 앱이다. 눈에 보이지 않기 때문에 LMK의 우선순위가 높다.)
9. 종료된 앱(안드로이드에서 뒤로가기로 종료해도 앱의 Process까지 종료되는 것은 아니다. 이유는 사용자가 다시 앱을 실행할 때 최대한 빨리 앱을 구동시켜 주기 위함이다.)
1~5까지는 강제 종료되면 사용자가 바로 인지할 수 있기 때문에 우선순위가 매우 낮다. 시스템에서는 이를 Forground Process라 부르고, LMK에 의해 왠만하면 죽지 않는다.
Low memory kill이 일어난다고 가정한 후 확인 해보니 택배 앱에서 배송 완료를 위해 카메라 앱에서 사진을 찍는 경우 메모리 부족 현상이 생기고 택배 앱이 우선 순위 8번인 상태이기 때문에 kill이 되어 생기는 문제로 확인 됐다.
이 문제를 해결 하기 위해 Sharedpreferences를 사용하여 앱이 kill 되도 데이터가 날아가지 않게 저장 하여 문제를 해결 했지만 Low memory kill을 위한 변수들이 Sharedpreferences에 선언 되는게 부자연스러워 보였고 해결 방법을 찾던 중 koin에서 제공 하는 stateViewModel을 찾게 됐다. 일반적인 viewModel도 config change가 일어날 때는 데이터를 보존할 수 있지만 Low memory kill이 일어나면 viewModel의 데이터도 같이 없어지기 때문에 savedStateHandle를 통해 state를 사용할 수 있는 stateViewModel 라는 viewModel을 제공한다.
결론적으로는 Sharedpreferences에 저장 하던 변수들을 제거한 후 stateViewModel을 koin을 통해 주입 받아서 savedStateHandle에 저장된 값을 꺼내서 사용 하여 문제를 해결 했다.