(3년차 안드로이드 개발자 반성기)Android ViewModel에서 SaveStateHandle을 활용하기

곽의진·2024년 10월 17일
3

Android

목록 보기
17/17

안녕하세요, 3년차 안드로이드 개발자 곽의진 입니다. 오늘 도입은 조금 제 부끄러운 개발 얘기를 써내려볼까 합니다...

근래에 새로운 앱의 Android 개발을 하면서 Paging3 Codelab을 참고하여 개발을 진행했었습니다. 저는 Paging3 기술에 대해 잘 알지 못했기에 Android Codelab을 참고해서 개발하게 되었습니다.

그런 과정에서 저는 아래 코드를 그대로 들고 오게되었습니다.

Paging Sample Code

class SearchRepositoriesViewModel(
    private val repository: GithubRepository,
    private val savedStateHandle: SavedStateHandle
) : ViewModel()

	init {
        val queryLiveData =
            MutableLiveData(savedStateHandle.get(LAST_SEARCH_QUERY) ?: DEFAULT_QUERY)

여기서 저는 savedStateHandle 을 통해 시스템에 의한 프로세스 종료시 데이터를 복구할 수 있다는 개념적인 의미는 알고있었지만 이를 실제로 테스트해보지는 않고 방어로직 차원에서만 추가한다는 생각으로 제 실제 프로젝트에 위 코드를 그대로 넣어두었습니다.

😐 무언가 잘못된 걸 깨달았다.

그리고 프로젝트 완성 후 제 코드를 보신분이 질문을 하시더라고요.

Q: 왜 savedStateHandle 을 넣으셨나요?
A: 음 시스템에 의한 프로세스 종료 시 데이터를 보존하기 위해서 입니다.

Q: 그러면 시스템에 의한 프로세스 종료에 대한 테스트는 어떻게 진행해보셨나요?
A: 엇.. 그 부분은 잘 모르겠습니다...

여기서 저는 앗.. 뭔가 잘못되었다 라는 걸 느꼈습니다. 저는 테스트도 해보지 않고 이럴 것 같은데? 라는 짐작만으로 코드를 여기에 넣게 된 것입니다.

3년차 안드로이드 개발자가 그럴 것 같은데? 라는 짐작만으로 어떤 코드를 넣는다는 사실이 굉장히 창피하고 반성해야겠다는 생각을 하며... 오늘 이 글을 쓰게 되었습니다.

이를 통해 앞으로는 똑같은 실수를 반복하지 않아야겠죠? 자 그러면 이 실수를 기회로 삼아 ViewModel에서 SaveStateHandle이란 무엇인지, 테스트는 어떻게 해야하는건지 확실하게 알아보는 시간을 가져보겠습니다.

ViewModel에서 SaveStateHandle

개발자는 사용자의 원활한 경험을 보장하기 위해서는 애플리케이션이 비정상적으로 종료되거나 구성 변경이 발생하더라도 UI 상태를 적절히 저장하고 복원하는 것이 중요합니다. 특히 현대의 Android 애플리케이션은 복잡한 UI 상태와 데이터를 관리해야 하는 일이 자주 발생하죠 😥

그렇기에 대부분 ViewModel을 활용하여 데이터의 상태를 보존하고, 액티비티나 프래그먼트의 생명주기 변화(예: 화면 회전)에도 데이터를 쉽게 유지합니다.

하지만 시스템이 앱 프로세스를 종료한 후에도 상태를 유지하고 복원하려면 조금 더 복잡한 SavedStateHandle API를 사용해야 하는데요, 이 글에서는 Android ViewModel에서 SavedStateHandle을 사용하는 방법과 그 이점을 알아보겠습니다.

UI 상태 저장 및 복원의 필요성

사용자 경험 유지

  • 중단 없는 경험 제공: 사용자가 앱 사용 중 전화 수신, 앱 전환 등의 이유로 앱이 백그라운드로 전환될 수 있습니다. 이때 앱이 다시 활성화되었을 때 이전 상태를 그대로 복원하면 사용자 경험이 향상됩니다.
  • 데이터 손실 방지: 입력 중이던 데이터나 진행 중이던 작업이 초기화되지 않도록 상태를 저장하여 데이터 손실을 방지할 수 있습니다.

다양한 종료 시나리오 대응

  • 시스템에 의한 프로세스 종료: 메모리 부족 등으로 인해 시스템이 앱 프로세스를 종료할 수 있습니다.
  • 구성 변경(Configuration Changes): 화면 회전, 다크 모드 전환, 언어 변경 등으로 인해 Activity나 Fragment가 재생성됩니다.

Activity의 종료 그리고 상태 저장 및 복원 시기

Android 시스템은 아래 공식문서에서 안내하는 것과 같이 메모리 부족과 같은 이유로 앱의 프로세스를 종료할 수 있습니다.

다만 시스템은 메모리 공간을 확보하기 위해 절대 Activity를 직접 종료하지 않습니다. 대신 Activity가 실행되는 프로세스를 종료하여 프로세스에서 실행되는 컴포넌트들을 간접적으로 종료시킵니다.

https://developer.android.com/guide/components/activities/activity-lifecycle.html?hl=ko#asem

이렇게 시스템에 의해 Activity가 종료된 경우 사용자가 이전에 보던 화면이나 입력했던 데이터를 그대로 복원하지 않는다면 사용자는 자신이 입력했던 정보를 처음 부터 쓰는 극악의 사용자 경험을 선사해주게 되겠죠?

SavedStateHandle 이란?

SavedStateHandle은 키-값 형태의 맵으로 데이터를 저장하며, Activity 또는 Fragment가 "onSaveInstanceState()" 메서드를 호출할 때 상태를 저장하며, 이후 프로세스가 다시 시작될 때 해당 상태를 복원합니다.

이를 통해 시스템이 프로세스를 종료하더라도, SavedStateHandle에 저장된 데이터는 일부 상태에서 살아남아 재사용될 수 있습니다.

다만 사용자가 앱을 강제로 종료하거나 Task에서 스택이 제거되는 경우에는 상태가 저장되지 않으므로, 이 점을 고려하여 중요한 데이터는 로컬 저장소를 통해 관리하는 것이 필요합니다.

SavedStateHandle을 이용한 상태 저장/복원 구현하기

SavedStateHandle은 ViewModel에서 상태를 저장하고 복원하는 데 사용되는 키-값 맵 형태의 데이터 구조입니다. 이를 통해 구성 변경뿐만 아니라 시스템에 의해 프로세스가 종료된 후에도 상태를 안전하게 복원할 수 있습니다.

기본 설정

Fragment 1.2.0 또는 Activity 1.1.0부터 ViewModel의 생성자에서 SavedStateHandle을 직접 사용할 수 있습니다. SavedStateHandle을 사용하는 기본 예제는 다음과 같습니다.

class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() {
    // 상태 저장 및 복원 로직 구현
}

class MainFragment : Fragment() {
    val viewModel: SavedStateViewModel by viewModels()
}

위와 같이 ViewModel의 생성자에 SavedStateHandle을 추가하면, ViewModelProvider는 자동으로 적절한 SavedStateHandle을 제공합니다.

만약 위 명시된 라이브러리의 이전 버전을 사용하신다면 공식문서를 참고해주세요

참 쉽죠? 하핳 쉽다고 그냥 코드를 넣으신다면 안됩니다... 이유를 알고 테스트를 해본 이후에 필요성을 느끼고 도입해야합니다. - 자기반성중 -

상태 저장 및 복원 예제

다음은 StateFlow와 함께 SavedStateHandle을 사용하는 예제입니다.

class SavedStateViewModel(
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    private val _sampleText = MutableStateFlow(savedStateHandle.get<String>("sample_text") ?: "")
    val sampleText: StateFlow<String> = _sampleText

    // sample text를 입력 시 savedStateHandle에 저장
    fun setSampleText(text: String) {
        _sampleText.value = text
        savedStateHandle["sample_text"] = text
    }
}

위 코드에서는 사용자가 입력한 검색어를 SavedStateHandle에 저장하고, 이를 이용해 필터링된 데이터를 가져옵니다. 이렇게 하면 시스템이 종료되었다가 다시 시작되더라도 동일한 검색 결과를 사용자에게 제공할 수 있습니다.

또한 SavedStateHandle은 LiveData나 Compose의 State API와 같은 관찰 Observable한 데이터 홀더와 함께 사용할 수 있습니다. 이를 통해 UI와의 상호작용 시 상태를 더욱 편리하게 관리할 수 있다고 하네요!!

SavedStateHandle 사용 시 유의사항

  • SavedStateHandle에 저장되는 데이터는 단순하고 가벼워야 하며, 복잡하거나 큰 데이터는 로컬 저장소(예: Room 데이터베이스)를 통해 관리하는 것이 좋습니다.

  • SavedStateHandle은 Activity가 stop될 때 데이터를 저장하므로, stop 상태 이후 추가적으로 데이터를 변경하더라도 저장되지 않을 수 있습니다. 따라서 추가적인 데이터 변경이 필요한 경우, Activity가 start 상태가 될 때까지 기다린 후 저장하는 것이 필요합니다.

테스트 방법

시스템에 의한 Activity가 종료되는 케이스를 테스트하는 방법이 바로 개발자 모드 > (액티비티)활동 유지 안함 옵션을 활성화 하면 된다는 것을 알았습니다!

이 옵션을 켜보고 테스트 해보니 꽤 대다수의 앱들이 이미 시스템에 의한 프로세스 종료에 대응 중인 것을 확인하고 아직도 저는 너무 많이 부족하다는 것을 깨닫게 되었습니다.

참고로 신기한 부분이 카카오톡에서 위 옵션을 키고 사진 첨부시에는 액티비티 유지안함 옵션을 꺼달라는 공지 팝업을 띄우고 있네요.

결론

결론적으로 ViewModel과 SavedStateHandle을 함께 사용하면 Configuration Change와 프로세스 종료에도 사용자에게 일관된 상태를 제공하여 더 나은 사용자 경험을 제공한다는 것을 깨달았습니다.

아! 그리고 제가 작성한 코드는 실제로 시스템에 의한 프로세스 종료시 복구가 안되더라고요...

맨날 말로만 사용자 경험~~ 어쩌구~~ 하다가 이런 예외를 만나게 되니 굉장히 부끄러워 지고 많이 배우게 된 하루였습니다.

아무튼 내일도 다들 즐거운 개발 화이팅하시길 바라겠습니다.

profile
Android Developer

3개의 댓글

comment-user-thumbnail
2024년 11월 9일

시스템에 의한 프로세스 종료시 복구가 안되는 이유는, queryLiveData가 변경될 때 savedStateHandle에
LAST_SEARCH_QUERY를 키값으로 set하지 않았기 때문일까요?

1개의 답글