본문 바로가기
Mobile : Android/Jetpack

[Jetpack AAC] 뷰모델(ViewModel) #1 : MVVM

by 신숭이 2022. 6. 13.

[Jetpack AAC] 뷰모델(ViewModel) #1 : MVVM

 

 

 

 

 

뷰모델(ViewModel)은 안드로이드에서 MVVM 패턴을 구현하고자 할 때 필수로 사용되는 AAC(안드로이드 아키텍처 구성요소) 중 하나다. MVVM은 모델(Model)-뷰모델(ViewModel)-뷰(View)의 책임 집합으로 나뉘는 설계 패턴으로, 안드로이드의 Jetpack에서 제공하는 뷰모델은 위와 같은 패턴을 구현할 때 많은 도움을 준다.

 

따라서 MVVM이 무엇이고 기존에 비해 나은 점이 무엇이며, 안드로이드에서 간단히 뷰모델 라이브러리를 사용하는 방법에 대해 리뷰하겠다. 뷰모델은 필연적으로 LiveData와 함께 쓰이는데, 이것은 다른 포스트에서 이어 서술하겠다.

 

 

 

1. MVVM 과 MVC

2. ViewModel 과 SIS(Saved Instance State)

3. ViewModel 기본 사용법

 

 

 

 

MVVM MVC

 

우선 Jetpack이 등장하기 이전, 안드로이드의 기본 프레임워크는 MVC(Model-View-Controller) 패턴을 지향했다. MVC의 안드로이드에서 모델(Model)은 데이터 클래스, 뷰(View)는 layout.xml 파일이 담당하고, 컨트롤러는 UI의 상태 갱신 및 비즈니스 로직을 수행하는 액티비티(Activity)또는 프래그먼트(Fragment)가 담당하였다. 

 

* MVC에 대한 좀 더 자세한 내용

 

[Pattern] Android의 설계 패턴 1 : MVC by Java

[Pattern] Android의 설계 패턴 1 - MVC Eric Maxwell 선생님께서 작성하신 안드로이드의 패턴 간단 정리를 통해 그동안 머릿속에 있던 안드로이드 설계 패턴에 대한 파편화된 기억을 모아보고자 한다

full-stack.tistory.com

 

MVC의 단점은 컨트롤러 역할을 하는 액티비티에 과도하게 책임이 집중되고, 그러다 보니 코드의 양도 비대해져 유지보수의 어려움이 생겼다. 단일 클래스가 이러한 모든 작업을 처리하려고 하면 테스트도 어려워지기 마련이다.

 

따라서 안드로이드의 MVVM은 기존에 액티비티가 수행하던 UI 작업과 비즈니스 로직을 분리하는 것이 기본적인 목표이다. View는 기존의 layout.xml과 Activity가 담당하여 사용자 작업에 반응하고, ViewModel은 UI에 필요한 상태 데이터를 관리하도록 한다. 모델은 Repository라는 클래스를 두어, 기존에 컨트롤러가 수행하던 비즈니스 로직과 데이터 로드(DB, Network)를 수행하도록 한다. 그림으로 그려보면 다음과 같다. (정확하진 않을 수 있지만 아마 맞을 것 같다.)

Jetpack으로 구현하는 안드로이드 MVVM 패턴

 

모델의 Repository는 기존 패턴에서도 많이 쓰이는 방식으로 DB나 네트워크로부터 데이터 로드를 담당하도록 구현 만 하면 된다. 뷰의 액티비티 또는 프래그먼트는 사용자의 작업에 대한 반응, 권한 요청 등의  OS 커뮤니케이션을 처리하고, 뷰모델은 UI의 상태 데이터와 UI와 관련된 로직들을 처리하도록 구현하면 된다.

 

 

 

 

 

ViewModel 과 SIS(Saved Instance State)

 

 

 

뷰모델이 위와 같이 책임을 분리하여 관리가 용이해지게 하는 것은 MVVM 패턴의 책임 집합으로서의 큰 장점이지만 안드로이드에서 ViewModel의 가장 큰 장점 중 하나는 액티비티나 프래그먼트의 생명 주기를 인식하여, 생명 주기와 무관하게 UI의 상태를 유지하도록 돕는 것이다.

안드로이드 시스템은 액티비티의 생명주기가 [중단(Stop)] 상태인 액티비티를 가진 프로세스에 대해 우선순위를 낮게 본다. (홈 버튼을 눌러 앱을 나간 뒤, 다른 앱을 실행) 우선순위가 낮은 프로세스는 안드로이드 시스템에 의해 언제든지 리소스가 회수될 수 있다. 리소스가 회수된다는 것은 UI 작업을 실행하는 스레드와 각종 객체들의 메모리들이  소멸되는 것을 말하며, 이러한 상황에서 사용자에게 일관된 화면을 보여줄 수 없게 만드는 요인이 된다.

프로세스 우선순위가 낮지 않더라도 액티비티 인스턴스가 소멸되는 경우가 있는데, 바로 장치를 회전할 때이다. UI상태에 대한 대응을 하지 않은 채 장치를 회전하면, 모든 UI 정보가 초기화되는 것을 목격할 수 있을 것이다. 이런 상태 정보의 소멸은 그동안 어느 정도 SIS(Saved Instance State)로 해결해왔다. 다음의 코드를 보자.

 

    private val KEY_VALUE = "KEY_VALUE"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val curValue = savedInstanceState?.getInt(KEY_VALUE) ?: 0

        ... 중략
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putInt(KEY_VALUE,1)
    }

 

SIS는 위와 같이 onSaveInstanceState 메서드를 오버라이드하여 사용할 수 있다. 상태를 유지하고 자하는 데이터를 Bundle 객체에 저장하여 onCreate()가 다시 호출될 때 불러와 UI 정보를 유지할 수 있다. 이 번들은 OS의 액티비티 레코드(Activity Record)라는 곳에 저장되고 이 액티비티 레코드는 앱이 완전히 종료되지 않는 한 유지될 수 있다.

 

그러나 SIS는 직렬화(Serialized)되는 방식으로 작동하기에 크거나 복잡한 객체를 저장하는데 적합하지 않다. 따라서 소규모 하드 코딩된 데이터나 사용자가 입력 중인 EditText 값 등에 적합하다. 그러므로 SIS는 동적인 대량의 데이터(네트워크나 DB 연동)에 적합하지 않고, 또한 이런 작업은 비동기적이기 마련인데 액티비티 생명주기와 결속되어 처리되면 작업이 복잡하기에 오류의 위험성에 노출되어있다.

 

Jetpack의 생명주기 인식 뷰모델(ViewModel)은 바로 이러한 필요성에 의해 등장하게 되었다. 

 

뷰모델은 onCleared() 호출 전까진 항상 상태를 유지

 

액티비티는 화면 회전 시, onPause -> onStop -> onDestroy -> onCreate의 과정을 거치지만, 뷰모델은 이러한 액티비티의 생명주기와 관계없이 상태를 유지한다. 액티비티가 onDestroy가 호출되고 완전히 상태가 [Finished]에 도달했을 때 뷰모델의 onCleared() 가 호출되어 종료된다. 프래그먼트는 상태가 [Detach] 일 때 호출된다. 즉 뷰모델은 자신에게 지정된 액티비티 또는 프래그먼트의 생명주기(Lifecycle)가 끝나는지 안끝나는지만 확인할 뿐, 그 외에는 일관되게 동작한다. 그리고 뷰모델은 기본적으로 싱글톤(SingleTone)으로 동작한다.


다만 뷰모델도 OS 우선순위에 의한 프로세스 소멸로 데이터를 잃을 수 있다. 이 때문에 SIS와 ViewModel은 적절히 함께 사용되어야 한다. 이에 대한 Jetpack의 최신 기술들도 있는데 이는 다음 포스트에서 작성하겠다.

 

 

 

 

ViewModel 기본 사용법

 

 

 

Jetpack은 뷰모델을 매우 간단하게 사용할 수 있도록 돕는다. 특히 Android KTX를 사용하면 Kotlin 개발자들에게 도움을 주는 다양한 확장 기능을 제공한다. 따라서 KTX를 사용하는 방식으로 작성해보겠다. Jetpack은 이제 안드로이드에서 기본으로 제공하지만, ViewModel 용 KTX는 따로 dependency를 추가할 필요가 있다.

 

 

build.gradle (App)

implementation 'androidx.fragment:fragment-ktx:1.4.1'

 

이렇게 하면 Kotlin의 위임자를 통해 다음과 같이 by 키워드를 통해 간단히 ViewModel을 선언할 수 있다. 

 

TestActivity.kt

class TestActivity : AppCompatActivity() {
    lateinit var binding: ActivityTestBinding
    private val testModel: TestViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this,R.layout.activity_test)

        binding.lifecycleOwner = this

		.. 중략

TestViewModel.kt

class TestViewModel : ViewModel() {

    val data1 = MutableLiveData(0)
    val data2 = MutableLiveData("value")
    
    .. 중략

 

나는 위와 같이 데이터바인딩과 연계하여 사용하였는데, 안드로이드 개발자 문서에는 데이터 바인딩을 사용하지 않고 아래와 같이 observe 하는 방식으로 사용하고 있다.

class MyActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same MyViewModel instance created by the first activity.

        // Use the 'by viewModels()' Kotlin property delegate
        // from the activity-ktx artifact
        val model: MyViewModel by viewModels()
        model.getUsers().observe(this, Observer<List<User>>{ users ->
            // update UI
        })
    }
}

 

뷰모델을 사용하다 보면, Context 객체의 필요성을 느낄 때도 있다. Context를 사용하고자 한다면, 다음과 같이 AndroidViewModel 을 사용하면 된다. 액티비티에서는 똑같이 위임자를 통해 선언하면 된다.

class TestViewModel(application: Application) : AndroidViewModel(application) {

	val context = getApplication<Application>()
	...
}

 

 

뷰모델은 프래그먼트 간 통신에도 탁월한 이점이 있다. 액티비티는 이러한 통신에서 전혀 개입하지 않아도 된다. 이렇게 작업하면 각 프래그먼트는 서로의 생명주기에 영향을 받지 않고 UI에도 영향을 미치지 않는다. 그리고 그냥 액티비티에서 사용 방식과 똑같이 위임자를 이용해 선언하면, 내부적으로 뷰모델을 싱글톤으로 작동시키므로 각 프래그먼트는 같은 뷰모델 인스턴스를 가지게 된다. 다음 코드는 안드로이드 개발자 문서에서 가져온 예시이다.

 

class SharedViewModel : ViewModel() {
    val selected = MutableLiveData<Item>()

    fun select(item: Item) {
        selected.value = item
    }
}

class ListFragment : Fragment() {

    private lateinit var itemSelector: Selector

    // Use the 'by activityViewModels()' Kotlin property delegate
    // from the fragment-ktx artifact
    private val model: SharedViewModel by activityViewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        itemSelector.setOnClickListener { item ->
            // Update the UI
        }
    }
}

class DetailFragment : Fragment() {

    // Use the 'by activityViewModels()' Kotlin property delegate
    // from the fragment-ktx artifact
    private val model: SharedViewModel by activityViewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        model.selected.observe(viewLifecycleOwner, Observer<Item> { item ->
            // Update the UI
        })
    }
}

 

 

 

 

 

 

댓글