실습 코드
값 보존하기
안드로이드 앱은 안드로이드라는 OS환경의 디바이스에서 설치되어 돌아가는 프로그램이다.
안드로이드는 메모리 부족과 같은 디바이스의 상황에 따라서 프로세스를 내리는 등의 OS로서의 역할을 수행하기 때문에 앱을 개발하면서 특히 이러한 부분의 고려를 생각해야 한다.
그렇지 않다면 겉보기에는 멀쩡하지만 툭하면 예상대로 안돌아가는 앱이 만들어지고, 코드는 문제 없는데 왜 안되는거야?와 같은 상황이 오기 일쑤다.
이러한 부분에서 생각해보자면 안드로이드에서 크게 생각할만한 부분은 Configuration Change와 Process Kill 정도다.
Configuration Change
한국어로 구성 변경으로 구성 변경은 기기 상태가 매우 급격하게 변경되어 시스템이 변경사항을 확인하는 가장 쉬운 방법이 활동을 완전히 종료하고 다시 빌드하는 것일 때 발생한다.
대표적 상황
- 앱 디스플레이 크기가 달라졌을 때 (폴드의 외부 디스플레이로 쓰다가 화면을 펼쳤을 때 같은)
- 화면 방향 전환 (가로, 세로)
- 글꼴 크기 및 두께
- 언어
- 다크/라이트 모드
이러한
구성 변경이 발생되면Activity는 생명주기 상onDestroy까지 호출되어 종료되었다가 다시 실행되게 된다.그렇기 때문에
Activity,Fragment에 값을 선언 및 저장한 후 그것을 UI에 뿌릴 데이터로 사용하게 되면, 화면 전환 등 구성 변경이 생길 때마다 값이 날라갔다가 다시 초기화 되버리는 상황이 나온다.// file: "MainActivity.kt" @AndroidEntryPoint class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() var cnt by mutableIntStateOf(0) setContent { RestoreTheme { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> ActivityScreen( modifier = Modifier.padding(innerPadding), cnt = cnt, onClickIncrement = { cnt++ } ) } } } } } @Composable fun ActivityScreen(modifier: Modifier = Modifier, cnt: Int, onClickIncrement: () -> Unit) { Column(modifier = modifier){ Text("Total Cnt : $cnt") Button( modifier = modifier, onClick = onClickIncrement ) { Text("Cnt ++ Button") } } }이런 간단한 버튼 누른 횟수를 보여주는 앱을 만들었을 때 버튼을 누를 때마다 횟수가 늘어나는 것이 잘 반영되지만 화면 전환과 같은 구성 변경이 발생하면 값이 초기화 된다.
구성 변경이 일어날 때마다 값이 날아가는 것을 볼 수 있다
선언별 차이
이러한 값 손실을 막기 위한 방법으로는 다양하게 있다.
ViewModel에 데이터를 저장하면Activity가 destroy 되더라도ViewModel은 파괴되지 않고 유지된다.단 중요한 점은
ViewModel을 정상적인 방법으로 호출해야 한다는 것이다.class MainActivity : AppCompatActivity() { // 잘못된 호출 val restoreViewModel: RestoreViewModel = RestoreViewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // ... } }위와 같이 생성자를 직접 호출하는 식으로 부르게 될 경우, 구성 변경이 일어났을 때
ViewModel은 위의 버튼 클릭 예제와 마찬가지로onDestroy될 때 사라졌다가,onCreate시 다시 호출되면서Activity에서 선언하는 것과 다를 바가 없게 되어버린다.올바른 호출 방식은
ViewModelStoreOwner를 바탕으로 생성을 하는 것이고,ViewModelProvider를 통해서 구현을 할 수 있는데 이를by viewModels()로 간단하게 부를 수 있다.또한
Composable의 인자로ViewModel을 부르는 경우viewModel()함수로 부를 수 있고 아래처럼LocalViewModelStoreOwner를 가져와서 세팅한다.
Composable의 viewModel() 함수 내부 구현 코드참고로
ViewModelStoreOwner는getViewModelStore메서드를 가지는Interface로ViewModelStore를 가지게 되며 이는String을Key로,ViewModel을value로 가지는Map을 가지고 있다.
ViewModelStoreOwner 일부
ViewModelStore 구조어쨌거나 이를 통해
ViewModel은 기본적으로 자신을 호출한 컴포넌트의 생명주기에 종속되는데도 불구하고,Activity가Destroy될 때 파괴되지 않을 수 있는 이유는 Acitivity 등의 로직에 있다ComponentActivity의 코드의 일부를 보면 아래와 같이OnDestroy이벤트가 일어났을 경우ConfigurationChange유무에 따라서 분기처리를 하고 결론적으로Configuration Change가 아닐 경우에는ViewModelStore의clear함수를 호출하지 않고 Destroy한다.
Configuration Change 여부에 따른 분기처리이
Configuration Change의 여부는 시스템에서 파악하며 이를 바탕으로Activity가 직접finish처럼 명시적으로 종료가 호출된 것인지 유무를 바탕으로ViewModel의 생존이 결정되게 된다.다시 정리하자면 결국 이 차이로 인해
Activity에 변수를 넣었을 때와는 다르게ViewModel에 변수를 넣고서 불러올 경우 화면 전환과 같은 구성 변경에서도 손실되지 않고 값을 계속해서 유지할 수 있다.다만 이건 어디까지나 구성 변경에 한해서 통용되는 이야기다.
Android단말기는 사용자가A앱을 실행시키고 있었다고 하더라도 전화가 오면A앱은 백그라운드로 밀리기 때문에 언제든지 어떤 앱이든 백그라운드로 돌아갈 가능성이 있으며 백그라운드는 포그라운드에 비해 우선 순위가 낮아 메모리가 부족해질 시 Kill될 가능성이 높아지기 때문에 우리가 실행하는 모든 앱은 항상 언제든지 OS에 의해 `Kill`될 염두에 두고 있어야 하며 그럼에도 불구하고 다시 돌아왔을 때 자연스럽게 이어 진행할 수 있어야 한다.그리고 결론적으로
ViewModel은 이러한 상황에서 해결책이 되어주진 않는다.어디까지나
ViewModel은Activity가Destroy될 때에도 죽지 않고 메모리에 살아 있는 것 뿐이기에 메모리가 부족해서Process가Kill되거나,Exception이 발생하는 등Process가 종료되고 메모리에서 회수되는 경우ViewModel또한 당연히 사라지게 된다.만약 최악의 경우 중요한 작업을 하고 있다가 전화가 와서 전화를 받고 돌아왔더니 앱이
Kill되어서Refresh되면서ViewModel에서 관리 중이던 데이터가 전부 날아갔다면? 상당한 문제가 될 것이다.Android에서는 이러한 경우를 위해서SavedStateHandle을 제공한다.정확히는
Activity에서는onSaveInstance에서Bundle에 값을Key - Value형식으로 저장하여Android System에 의해서Process가Kill되었을 때 다시 복구할 수가 있는데,ViewModel의 데이터를Activity의onSaveInstance에서 저장할 필요 없이 저장하기 위한 방식이다.더 정확히는 과거
Activity나Fragment의onSaveInstance에서 저장 및onRestoreInstanceState에서 복구하던 것을Activity나Fragment에서만 해야 할 필요 없이SavedStateRegistry,SavedStateRegistryController,SavedStateProvider를 통해서 원하는Component에서UI Conroller(Activity, Fragment)에Hook을 걸어서 상태 저장 및 복원 과정에 참여할 수 있게 한다.역할\이름 SavedStateRegistryOwner SavedStateRegistry SavedStateRegistryController SavedStateRegistryProvider Registry를 보유하는 주체컴포넌트에서UI Controller에 연결하여 상태 저장 및 복원 과정에 참여할 수 있도록 함Registry에Provider들을 등록하여 하나의Registry에서 여러Provider들을 관리SavedStateRegistry를 Control 하는 것performSave,performRestore메서드들을 이용해서 상태를 저장 및 관리saveState()라는Bundle을 return하는 함수 하나가 정의된 인터페이스SavedStateRegistry는 연결된UI Controller의Lifecycle에서State를 저장하는 단계일 때Provider의saveState메서드를 호출하여 상태 저장어쨌거나
Component Activity에서SavedStateOwner를 상속받아 관련ViewModel을 만들 때SavedStateHandle을 사용할 수 있고Bundle에 담겨진다.중요한 건 `SavedStateHandle`이 계속해서 살아남는게 아니라 `SavedStateHandle`은 보관할 값들을 가지고 있다가 전달해주는 역할에 불과하다는 점이다.
SaveStateHandle, 이 또한 ViewModel이 생성될 때 create로 생성되는 것이다.이 또한 앱 내의 메모리 영역에 해당하는 객체로 프로세스가
Kill이 되어 메모리 해제가 되면 같이 사라지는 데이터이다.여기서 한가지 또 인지해야 할 것은 이러한
Saved~~로 값을 처리하는 작업들은 모두 Main Thread에서 일어난다는 점이며, 무거운 직렬화, 역직렬화는 지양해야 한다.결국
SavedStateHandle은Bundle에 저장할 값들을 담아주는 중간 역할을 한다.그렇다면
Bundle에 담긴 데이터들은 도대체 어디로 가길래 안전한 것일까?이에 대해 조사해보아도 공식 문서에서 명확한 안내는 없었는데, 자료를 찾아보다
StackOverflow에서 링크된 관련 포스팅에서 내용을 확인할 수 있었다.This icicle-bundle is the saved instance state and it is actually stored in the memory space of the Activity Manager service.
이에 따르면
Activity Manager Service라는 곳의 메모리 영역에서 저장하고 있다는 것을 확인할 수 있다.그렇다면
Activity Manager Service는 무엇일까Activity Manager Service는Activity,Service같은 컴포넌트들을 관리하고,Activity Stack을 이용해lifecycle을 관리하는 등Android의 핵심적인 역할을 담당한다.결론적으로 메모리에 있지만 앱이 아닌 AMS 내부에서 관리되기 때문에
Process Kill이 되더라도 정보가 유지되어 복원할 수 있다.다만 사용자가 직접 최근 실행 목록에서 밀어서 닫는 등 명시적인 종료 or 재부팅의 경우에는 복원되지 않는다.
또한 메모리에 저장되는 것은 동일하며, 앞서 말했듯이 Bundle 관련 직렬화, 역직렬화 하는 과정이 Main Thread에서 처리되기 때문에 복잡한 직렬화 과정이 필요하거나 큰 데이터를 저장하는 것은 피해야 하며, 만약 재부팅 등 그 어떠한 상황에서도 데이터의 보존이 필요한 경우에는
Room등을 통해Disk에 직접 기록할 필요가 있다.Compose에서는State에 변경이 생겼을 시Recomposition이 발생하면서 다시 화면을 그리게 되는데, 구성 변경 시에 Activity가 다시 실행되듯이Composable이 다시 실행되면서 내부의 값이 날아가게 된다.이처럼
Recomposition시remember로 감싸서 값이 날아가는 것을 방지할 수 있고,rememberSavable을 이용해서 앞서 말한Process Kill상황에서도 값이 살아있게 할 수 있다.특히 주의할 점은 내부에서 쓰는 상태 변수를 remember를 사용하지 않는 경우인데, 이 경우 상태 변수를 업데이트 해주었더고 하더라도 다시
Recomposition이 되고Composable이 다시 구성되기 때문에 값이 날아간다.State가 아니더라도 날아가는 것은 마찬가지다다행히 이런 경우를 방지하고자
Android Studio에서 아래와 같은 알림을 띄워주기 때문에 실수하더라도 쉽게 파악할 수 있다
Android Studio에서 알림을 띄워준다.또한 앞서 말한
Process Kill의 경우 휴대폰의개발자 모드에서활동 유지 안함을 활성화 할 경우 홈 버튼을 눌러 나가는 것만으로 발생시켜 테스트를 할 수 있다.
활동 유지 안함결과적으로
Recomposition,Configuration Change,Process Kill상황에 대해서 값의 보존에 대한 내용을 알아보았다.정리를 하면 아래의 표와 같으며 이를 테스트 해보기 쉽도록 만든 프로젝트 파일과 실제로 테스트한 영상을 첨부하는 것으로 이번 글을 마친다.
상황 \ 방법 Activity에 변수 선언 remember 없이
Composable 내 변수 선언remember rememberSavable ViewModel SavedStateHandle Recomposition O X O O O O Configuration Change X X X O O O Process Kill X X X O X O 상황과 방법별 값 보존 유무
Recomposition, Configuration Change, Process Kill마다 값