서론
이번 포스트에서는 ViewModel을 사용은 하지만, 내부동작은 어떻게 이루어지는가에 대해서 잘 모르는 사람들을 위해 알아보려고 합니다(저포함)! 보통은 그냥 by viewModels()
를 이용해 사용하는 방법만 알 뿐, 그 안에서는 어떤식으로 동작하는지는 크게 신경쓰지 않습니다.
그럼 이제 ViewModel에 대해서 먼저 알아본 뒤, 하나하나 따라가보겠습니다!
다만 이 포스트는 viewModels의 기본 사용법이므로, 아래와 같이 viewmodelFactory는 따로 만들어주지 않았습니다.
viewModelFactory를 넣어주는 예시
private val githubViewModel: GithubViewModel
by viewModels(factoryProducer = { GithubViewModelFactory(RemoteDataSourceImpl(), this) })
ViewModel vs AAC ViewModel
우리는 안드로이드에서 MVVM 패턴을 구현할 때, ViewModel을 사용합니다. 그런데 우리가 안드로이드에서 사용하는 ViewModel은 보통 AAC(Android Architecture Components) ViewModel이라고 부르는데, 이는 MVVM 패턴의 ViewModel과는 사실 같지(=완벽하게 같은 의미) 않습니다.
MVVM에서 설명하는 ViewModel은 View에서는 UI를 나타내는 것에만 집중하고, 그러한 View를 돕기 위해 데이터와 메소드를 ViewModel에 구현하고, 이를 옵저버 패턴을 구현해 상태 변화를 View에게 알려줍니다. 또한 Model 층을 하나 더 두어 비즈니스 로직을 따로 분리해내면 테스트와 유지보수가 쉬운 MVVM 패턴이 완성됩니다.
AAC ViewModel은 Android의 생명주기를 고려하여 UI 관련 데이터를 저장하고, LiveData와 Databinding을 통해 View로 데이터의 변화를 알려줍니다. 기존에 생명 주기로 인한 어려움을 겪었다면, 이를 이용하면 쉽게 해결이 가능합니다.
이 두개를 요약하자면, MVVM에서의 ViewModel은 View에 필요한 데이터를 관리하고 바인딩해주고, 비즈니스 로직과 뷰를 연결해주면서 동시에 분리하는 것이고, Android AAC ViewModel은 Android의 수명주기를 고려하며 UI 관련 데이터를 저장하고 관리하는 요소라고 볼 수 있습니다.
즉, MVVM 에서 ViewModel은 연결과 분리의 의미가 강하다면, AAC ViewModel은 UI 관련 데이터의 저장의 의미가 강하다고 볼 수 있습니다.
이제 보니 비슷하지만, 엄연히 구분하자면 다른 ViewModel로 볼 수 있겠죠? 하지만 MVVM ViewModel과 AAC ViewModel의 공통점을 보자면 둘 다 View에 필요한 데이터를 가지고, 관리해주는 역할을 할 수 있다는 점입니다.
그래서 AAC ViewModel을 이용하면, MVVM의 ViewModel을 구현해줄 수 있게 되는 것입니다. 안드로이드에서의 LiveData와 DataBinding을 함께 이용해주면, UI의 데이터를 바인딩 해주고, 안드로이드 Lifecycle또한 고려하여 ViewModel을 만들어낼 수 있기 때문에 AAC ViewModel을 간편하게 MVVM ViewModel과 동일하게 보고 이용하고 있던 것입니다!
앞서 ViewModel의 정의해 대해서 짧지만 이해가 쉽게 작성해봤습니다. 이제 AAC ViewModel(이하 ViewModel)이 어떻게 동작하는지 하나하나 알아보겠습니다!
ViewModel의 동작
현재 ViewModel을 쓸 때 아래와 같이 사용한다는 것은 모두 알고 계실겁니다.
private val mainViewModel: MainViewModel by viewModels()
이러한 Kotlin의 위임패턴(by)을 이용한 viewModel의 인스턴스 초기화는 사실 내부적으로 이전에 사용했던 ViewModelProvider()를 이용한 인스턴스 초기화 방식과 동일합니다. 그저 사용하기 쉽게 Lazy 패턴을 이용해 만든 것 뿐입니다 (사실 이를 통해 더욱 쉽게 전역 변수로써 사용이 가능하고 메모리상의 이득을 본다고 생각합니다만.. 깊게 이야기하면 길어지니 줄이겠습니다...).
그래서 마지막 부분에서 위임 패턴(by)
과 by viewModels()
에 대해서 설명하고, 이제부터는 ViewModelProvider()를 통한 초기화를 해보도록 하겠습니다.
1. ViewModelProvider()
private val mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
override fun onCreate() {
...
}
위와 같이 전역변수로 사용하기 위해 onCreate() 메소드 위에 viewModel을 작성해줄 경우 아래와 같은 에러를 만날 수 있습니다.
글을 읽어보면, 액티비티가 아직 어플리케이션의 인스턴스에 붙지 않았으니, onCreate() 메소드가 호출되기 전에 사용하지 말라고 나옵니다. 이를 해결하려면 onCreate() 안에서 초기화해주거나, lateinit으로 나중에 초기화를 시켜줘야겠죠? 이보다 더 깔끔하고 좋은 방법이 위에서 Lazy 패턴을 이용한 초기화입니다.
private val mainViewModel: MainViewModel by viewModels()
하지만 처음에 말했던대로 ViewModelProvider를 이용해보겠습니다.
override fun onCreate() {
...
val mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
}
ViewModelProvider의 생성자는 총 3개이며, 첫 실행시 ViewModelStoreOwner라는 인스턴스만 전달해주고 아래 생성자가 실행되게 됩니다.
그렇게되면 생성자를 통해 객체를 생성하게 되는데, owner.viewModelStore는 아시다싶이 getViewModelStore()라는 메소드를 호출합니다.
2. getViewModelStore()
이 메소드는 처음에 봤던 onCreate() 위에 선언시 Exception을 뱉어내는 역할을 합니다.
후에 ensureViewModelStore() 메소드를 진행합니다.
3. ensureViewModelStore()
이 메소드는 viewModelStore가 null일 경우 새로 만들어주는 역할을 합니다. 그런데 보통 우리가 생성자로 넘겨준 this(Activity)는 ComponentActivity를 상속받고 있는데, 이는 ViewModelStoreOwner를 구현하고 있고, FragmentActivity 또한 그렇습니다. 이를 통해 우리는 첫 실행시에도 mViewModelStore의 값을 가질 수 있고, 해당 함수에서는 null이 아니기 때문에 ViewModelStore 객체를 새로 생성하지 않고 넘어가게 됩니다.
후에 해당 함수는 끝이나고 다시 getViewModelStore()로 이동합니다.
4. getViewModelStore()
위의 2번 함수에서 생성된 ViewModelStore를 리턴해주고, 다시 생성자 선언부로 돌아갑니다!
5. defaultFactory()
defaultFactory() 메소드는 ViewModelProvider 클래스 최하단에 companion object로 선언되어 있습니다. 여기서 ViewModelProviderFactory가 커스텀인지, default provider factory인지 구분하여 리턴해줍니다. ViewModel Provider Factory를 커스텀해보지 않아서 잘은 모르겠습니다...
if문의 owner.defaultViewModelProviderFactory
를 통해 getDefaultViewModelProviderFactory() 메소드가 실행됩니다.
6. getDefaultViewModelProviderFactory()
현재 ViewModelStore는 있지만, ViewModelFactory는 null이기 때문에 아래 if문안에서 SavedStateViewModelFactory를 통해 ViewModleFactory 객체를 생성하여 결과로 넘겨줍니다.
7. SavedStateViewModelFactory()
SavedStateViewModelFactory는 ViewModelFactory 인스턴스를 실제로 생성해주는 곳입니다. 수 많은 인자들 중에서 mFactory
를 보면 application이 null이 아닌 경우(아까부터 계속 나오지만, onCreate() 안에서 또는 후에 사용하면 null이 아닙니다!) ViewModelProvider.AndroidViewModelFactory.getInstance를 이용해 인스턴스를 생성합니다.
생성자가 아닌 실제 SavedStateViewModelFactory() 클래스는 ViewModelProvider의 KeyedFactory를 상속받고 있습니다!
8. AndroidViewModelFactory.getInstance
이 메소드는 아까 defaultFactory가 위치해있던 ViewModelProvider 최하단 companion object 내에 위치해있습니다. 싱글턴으로 인스턴스를 반환해줍니다. 이때 AndroidViewModelFactory를 보겠습니다.
9. AndroidViewModelFactory
AndroidViewModelFactory는 NewInstanceFactory()를 상속받고, create()메소드를 이용해 ViewModel의 인스턴스를 생성해주는 역할을 담당합니다.
modelClass라는 파라미터는 뷰모델의 인스턴스로, 위의 isAssignableFrom()이라는 메소드는 파라미터로 넘어온 viewModel 클래스가 AndroidViewModel을 상속했는지 확인하는 메소드로 상속 되었다면 true를 리턴합니다. 자바의 instanceof() 메소드와 유사하며, 차이점은 instanceof는 특정 Object가 클래스, 인터페이스를 상속, 구현 했는지, isAssignableFrom() 메소드는 특정 Class가 클래스, 인터페이스를 상속, 구현했는지입니다.
하지만 저희는 AndroidViewModel()을 상속받지 않았으니 아래 super.create()가 실행될 예정입니다. 보통 뷰모델 안에서 context가 필요할 때 AndroidViewModel()을 사용합니다!
물론 create() 메소드가 바로 실행되진 않습니다! create() 메소드가 호출 됐을 때 실행이 됩니다. 다만 ViewModelFactory가 ViewModel의 인스턴스를 생성한다는 점을 강조하기 위해 미리 설명을 했으니 이해 부탁드립니다.
10. NewInstanceFactory()
AndroidViewModelFactory()는 NewInstanceFactory()를 상속받고 있어 위의 코드가 실행됨과 동시에 NewInstanceFactory()가 실행됩니다. 여기서도 create() 메소드만 가지고 있으며, 9번 사진에서의 else 문에서 super.create()의 경우 이곳의 create()가 실행됩니다.
후에 8번으로 돌아가 인스턴스를 리턴해줍니다. 여기서는 그냥 AndroidViewModelFactory의 인스턴스를 가지고 있다고만 이해하면 될것 같습니다.
이렇게 7번으로 다시 돌아가 mFactory의 인스턴스가 AndroidViewModelFactory로 초기화됩니다.
11. getDefaultViewModelProviderFactory()
다시 이는 6번으로 돌아가 생성된 ViewModelFactory를 리턴해줍니다.
12. defaultFactory()
이는 다시 5번, ViewModelProvider에서 생성하고 있던 defaultFactory() 메소드를 완료합니다.
13. 생성자
결국 이제 1번이 끝났습니다. 지금까지 defaultFactory()를 통한 viewModelStore와 viewModelFactory의 초기화가 끝났습니다. 이제 ViewModelProvider의 생성자에 초기화한 값들이 연결됩니다.
14. 다시 Activity로
이제 처음의 코드에서 ViewModelProvider(this)가 끝났습니다.
val mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
이제 다음 get() 메소드를 이용한 ViewModel 인스턴스 생성을 보겠습니다.
15. ViewModelProvider.get() - 1
ViewModelProvider의 get() 메소드를 통해 ViewModel 클래스를 전달해주면, get() 메소드 안에서 ViewModel 클래스의 이름과 객체로 다른 get() 메소드를 호출합니다.
16. ViewModelProvider.get() - 2
위의 get() 메소드 바로 밑에 있는 get() 메소드 입니다. 이곳에서 파라미터를 먼저 볼텐데, key라는 파라미터로 ViewModel의 이름을, modelClass라는 파라미터로 ViewModel 클래스를 가지고있습니다.
이전에 생성해둔 ViewModelStore에 key(ViewModel 이름)를 통해 값을 가져옵니다. 하지만 첫 실행이기에 viewModel이라는 변수에 null이 할당됩니다.
그렇게 첫 번째 if문은 지나갑니다.
그리고 viewMode을 초기화해주는 부분에서 if(factory is KeyedFactory)
를 통해 factory.create(key,modelClass)
가 실행됩니다. (위에 설명했듯이 factory의 인스턴스인 SavedStateViewModelFactory가 KeyedFactory를 상속받고있습니다)
17. factory.create()
사진에서도 보이지만 저희는 AndroidViewModel이 아니기 때문에 else문의 findMatchingConstructor(modelClass, VIEWMODEL_SIGNATURE)이 실행됩니다.
위 함수는 null을 리턴하며, 무슨 함수인지 잘 모르겠습니다..
후에 아래 if문에서 constructor == null을 통해 mFactory의 create() 메소드의 인자로 ViewModel 클래스를 줍니다.
18. AndroidViewModelFactory.create()
이전에 설명했던 create() 메소드에서 AndroidViewModel인지 체크를 합니다. 물론 저희는 아니기때문에 else문의 super.create()가 실행됩니다.
super.create()는 10번 NewInstanceFactory 클래스의 create()가 실행됩니다.
19. NewInstanceFactory.create()
이제 ViewModel의 인스턴스를 리턴해줍니다.
이제 다시 하나씩 거슬러 올라가 16번, viewModel 인스턴스에 ViewModel 클래스의 인스턴스가 저장됐습니다. 이제 16번의 코드를 계속 진행하여 ViewModelStore에 ViewModel의 클래스 이름과 객체를 저장해줍니다.
20. ViewModelStore
중요한 ViewModelStore 입니다. ViewModelStore에서는 ViewModel을 HashMap으로 key와 value형태로 저장하고 있습니다. key에는 ViewModel의 이름으로, value에는 객체를 저장하고 있습니다.
여기서 oldViewModel은 null 값을 가지고 있는데, hashMap의 put은 이전의 value를 리턴해줍니다. 즉, 첫 실행인 경우에 put의 결과값으로 null이 나오게 됩니다. 그리고나서 아래의 if문은 지나갑니다.
후에 ViewModelStore에 현재 ViewModel을 저장하고, 19번의 viewModel을 리턴해줍니다.
21. ViewModelProvider.get -2 리턴
하나씩 거슬러 올라가며 get() 메소드를 끝내 ViewModel 객체를 리턴해줍니다. 이를 통해 아래의 코드를 모두 끝마쳤습니다.
val mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
22. 이제 사용하자!
이제 ViewModelStore에 현재 ViewModel이 HashMap으로 key, value 형태로 저장됐습니다.
이제 Activity에서 사용할 일만 남았습니다.
이제 한줄로 정리해보겠습니다.ViewModelProvider를 통해 ViewModel을 요청해서, ViewModelFactory를 통해 인스턴스를 생성하고, ViewModelStore에 저장한 뒤에, 꺼내서 쓰자!
22. 언제 없어지는데?
아시다싶이 ViewModel은 화면 회전과 같은 Configuration Change에서도 살아남습니다. 또한 onDestroy() 이후 onCreate()가 불려도 생존해있기도 합니다. 어떻게 해야 사라질까요?
20번 ViewModelStore의 clear() 함수를 보겠습니다!
위의 주석을 보면 내부 저장소를 지우고, 더이상 사용하지 않음을 알린다고 합니다.
이 clear() 메소드는 언제 호출될까요?
두 군데서 호출이 되고 있는데, 한 곳은 액티비티, 한 곳은 프래그먼트입니다!
액티비티를 먼저 보죠!
LifecycleEvent가 ON_DESTROY일 때, if문을 검사해줍니다. 현재 ConfigurationChange가 일어났을 경우에는 ViewModel의 인스턴스를 clear() 하지 않고 가지고 있습니다. 즉, 화면 회전같은 단순한 ConfigurationChange의 경우에는 ViewModel의 인스턴스를 그대로 유지하고 있겠다는 뜻이죠!
이번에는 프래그먼트에서의 사용을 보겠습니다. 프래그먼트는 조금 사진이 많으니 잘보셔야 합니다! 하나하나 거슬러 올라가보겠습니다.
clearNonConfigStateInternal()라는 메소드에서 clear()를 진행해주고 있습니다. 프래그먼트의 정보들을 지워주고 있는 것 같네요. clearNonConfigStateInternal()를 호출하는 메소드는 그 위에 작성되어 있습니다.
이 두함수 또한 호출되는 곳을 가보겠습니다.(두 함수의 차이는 프래그먼트를 넘겨주나, 프래그먼트의 고유 이름을 넘겨주나 차이입니다)
이곳이네요! shouldDestroy 라는 값을 통해 확인한 뒤 mFragmentStore의 값을 clear해주게끔 수행합니다.
Fragment의 Lifecycle이 onDestroyView -> onDestroy -> onDettach 순서로 이루어질 때, onDestroyView에서 ViewModel의 인스턴스는 살아남고, onDestroy시에 인스턴스가 clear() 된다고 볼 수 있겠습니다.
다만 구글에서 제시한 ViewModel의 Lifecycle을 보겠습니다.
구글이 제시한 ViewModel의 Lifecycle을 보면 onDestroy() 이후에도 살아있는 것을 확인할 수 있는데, 아래의 사진을 보면 ViewModel안의 onCleared() 메소드는 onDestroy() 이전에 호출이 되는 것을 확인할 수 있습니다.
액티비티의 상태에 따라 configuration change가 아니라면 ViewModel을 clear() 해주지만, 아닌 경우에는 ViewModel의 인스턴스가 유지됩니다. 그래서 제가 생각하기에는 ViewModel이 액티비티의 Lifecycle보다 생명주기가 길다기 보다, 조건에 따라서 더 오래 산다고 보는 것이 맞다고 생각합니다.
마지막 ViewModel의 Lifecycle은 틀린 부분이 있을 수도 있습니다. 오류나 오타는 댓글로 지적 부탁드립니다!
지금까지 ViewModel 파헤치기였습니다. 마지막 부록으로 by 키워드를 통한 ViewModel의 초기화에 대해서 정리하고 마무리 하겠습니다.
by viewModels()
위의 포스트에서는 ViewModel의 기본 생성 방법을 통해 ViewModel의 객체를 선언하는 방법을 알아봤습니다. 그런데 요즘의 ViewModel을 사용할 때는 간편하게, by
키워드를 이용하곤 합니다.
private val viewModel: MainViewModel by viewModels()
위와 같이 선언할 경우, Lazy 객체가 viewModel 인스턴스에 할당되게 됩니다.
위와 같이 ViewModelLazy 라는 객체가 할당되고, 아시다 싶이 이 객체(viewModel)이 처음 사용될 때 초기화를 수행하게 됩니다.
위의 ViewModelLazy는 처음에 수행되지 않고, 값을 얻기 위해 객체에 접근시에 커스텀 게터를 이용해 초기화를 진행합니다. 첫 실행이기에 viewModel 객체는 null이고, ViewModelFactory는 factoryProducer(), ViewModelStore()은 storeProducer()를 통해 생성하여 일반 ViewModelProvider와 동일하게 진행해줍니다. 디버깅을 통해 생성과정을 따라가보면 일반 ViewModel 객체를 생성하는 것과 동일합니다. Factory와 Store를 생성하여 Provider에 넣어주는 것이 동일합니다. 다만 Lazy Pattern을 이용해 사용시에 초기화 한다는 점만 다릅니다.
코틀린에서의 Lazy 패턴은 사용시에 변수가 초기화된다는 장점이 있습니다. by lazy 키워드 다음 { } 안에서 지정해준 코드를 사용시에 초기화해줄 수 있죠. 이를 통해 확장 함수와 합쳐져서 viewModels()를 이용해 쉽게 사용할 수 있는 것입니다.
by viewModels는 내부를 살펴보면 ViewModelLazy를 리턴하는 확장함수로 만들어져있습니다.
이는 사실 우리가 액티비티에서 다음과 같은 코드를 한줄로(val viewModel:MainViewModel by viewModels()
)사용하게 하기위해서 만든 것입니다. 사실상 이런식으로 구성이 되어있는거죠
class MainActivity: AppCompatActivity(){
val viewModel: MainViewModel by lazy {
ViewModelLazy(MainViewModel::class.java, viewModelStore,
defaultViewModelProviderFactory)
}
private fun ViewModelLazy(
viewModelClass: Class<MainViewModel>,
storeProducer: ViewModelStore,
factoryProducer: ViewModelProvider.Factory
): MainViewModel {
return ViewModelProvider(storeProducer, factoryProducer)[viewModelClass]
}
...
}
lazy 패턴을 통해 사용시에 한번만 초기화 하는데, 그 때 ViewModelLazy라는 함수를 통해 ViewModelProvider로 ViewModel 객체를 생성해서 할당하는 겁니다. 다만 이러한 코드가 너무 길어서 viewModels
라는 확장함수 안에 이러한 생성을 위임하는 겁니다.
또는 위에보다 짧게 확장함수만 건너뛰고 바로 클래스에 값을 전달해줄 수 도 있죠!
val viewModel: MainViewModel by ViewModelLazy(MainViewModel::class, { viewModelStore },
{ defaultViewModelProviderFactory })
또한 by viewModels
로 생성해 ViewModelLazy 객체를 가지고 있는 viewModel 객체는 값을 가져올 때마다, 커스텀 getter, 즉 get() 함수에서 값을 가져온 다는 점을 확인해보면 좋을 것 같습니다.
이해가 가셨나요??? 질문, 지적은 댓글로 부탁드립니다!
부록 activityViewModels()
Fragment에서 사용하는 activityViewModels는 어떻게 상위 액티비티와 ViewModel을 공유할까요?
내부 구조를 보면 requrieActivity()를 통해 부모 액티비티의 인스턴스를 가져와 그 안의 ViewModelStore와 ViewModelProviderFactory를 가져옵니다. 이를 통해 ViewModelLazy 객체를 생성하는데, 즉 부모 액티비티와 동일한 ViewModelStore, ViewModelProviderFactory를 통해서 인스턴스를 얻기 때문에 동일한 ViewModel을 사용할 수 있게 됩니다.
출처 및 참고
https://leveloper.tistory.com/216
https://black-jin0427.tistory.com/322
https://charlezz.medium.com/viewmodel%EC%9D%B4%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80-viewmodel-%EC%B4%88%EB%B3%B4%EB%A5%BC-%EC%9C%84%ED%95%9C-%EA%B0%80%EC%9D%B4%EB%93%9C-e1be5dc1ac18
https://developer.android.com/topic/libraries/architecture/viewmodel?hl=ko#lifecycle
https://wooooooak.github.io/android/2020/10/11/AAC_VewModel_internal/
'Android' 카테고리의 다른 글
LifecycleScope, ViewModelScope의 내부 구조 (0) | 2022.09.14 |
---|---|
AAC ViewModel은 어떻게 onDestroy에서 살아남을 수 있었을까 (0) | 2022.09.14 |
Fragment의 Lifecycle 잘 알고 있나요 (0) | 2022.09.13 |
Coroutine과 Retrofit을 같이 사용하면 enqueue를 쓰지 않아도 되는 이유는? (3) | 2022.09.13 |
RecyclerView ListAdapter를 통한 DiffUtil 사용법 (0) | 2022.09.13 |