ViewModel에 대해 알아보자 | Android Study

hoyaho·2021년 12월 20일
0

Android Study

목록 보기
10/14
post-thumbnail

🤔 ViewModel?

ViewModel 클래스는 생명 주기를 고려하여 UI 관련 데이터를 저장하고 관리하도록 설계되었습니다. ViewModel 클래스를 사용하면 화면 회전과 같이 구성을 변경할 때도 데이터를 유지할 수 있습니다. - Google Android

굉장히 직관적인 설명이다. 예를 들어 설명해보자. 지난번 Livedata에 관해 포스팅 했는데, 그것을 예시로 들어보자.

당시 작성했던 코드에서, 화면 전환을 하면 데이터가 모두 초기화되는 모습을 확인할 수 있다. 이유는, 안드로이드 생명주기와 관련이 있다.

기본적으로 안드로이드는 화면이 전환되면 액티비티가 Destroy 되었다가 다시 Create 되기 때문에, 설정 데이터가 모두 날아가 기존 XML에 설정한 텍스트가 화면에 나타나게 되는 것이다. 이러한 경우 원래는 onSaveInstanceState() 메소드를 사용하여 onCreate()에서 데이터를 복원할 수 있었다.

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

        if (savedInstanceState != null) {
            var nickname = savedInstanceState.getString("currentNickname")!!
            var name = savedInstanceState.getString("currentName")!!
            binding.tvNickname.text = nickname
            binding.tvName.text = name
            var userData = User(nickname, name)
            userLiveData.postValue(userData)
        }
    }
    
    override fun onSaveInstanceState(outState: Bundle) {
        userLiveData.value.let {
            outState.putString("currentNickname", it!!.nickname)
            outState.putString("currentName", it.name)
        }
        super.onSaveInstanceState(outState)
    }

이런식으로 작성을 하여 사용할 수 있었으나, 공식 문서에서는 다음과 같이 이야기하며 사용을 말리고 있다.

  • 이 접근 방법은 사용자 목록이나 비트맵과 같은 대용량일 가능성이 높은 데이터가 아니라, 직렬화했다가 다시 역직렬화할 수 있는 소량의 데이터에만 적합합니다.
  • UI 컨트롤러가 반환하는 데 시간이 걸릴 수 있는 비동기 호출을 자주 해야 한다는 점입니다. UI 컨트롤러는 비동기 호출을 관리해야 하며, 메모리 누수 가능성을 방지하기 위해 호출이 제거된 후 시스템에서 호출을 정리하는지 확인해야 합니다. 관리에는 많은 유지관리가 필요하며, 구성 변경 시 개체가 다시 생성되는 경우 개체가 이미 수행된 호출을 다시 호출해야 할 수 있으므로 리소스가 낭비됩니다.

즉, 담을 수 있는 데이터의 크기가 적을 뿐더러 형태마저 고정되어 있고, UI 컨트롤러에 많은 책임이 과중되어 관리가 어려워지니 사용하지 말라는 이야기이다.

더 쉽게 이야기하자면, 액티비티와 프래그먼트가 다 죽기 전에, 역할좀 분담하라는 뜻이 되겠다. 그래서 데이터를 따로 분담하기 위해 나타나게 된 것이 바로 ViewModel이다.


위 사진을 보면 알겠지만, ViewModel의 경우 액티비티가 Destroy 되더라도 삭제되지 않고 메모리에 남아있는 모습을 확인할 수 있다. 프래그먼트도 마찬가지, 프래그먼트가 분리될 때까지 ViewModel은 메모리에 남아있다. 이렇게, 액티비티 혹은 프래그먼트가 기기의 구성변경(예 : 화면 회전)에 의해 onCreate를 여러번 호출하더라도, ViewModel은 자신을 호출하는 UI 컨트롤러보다 생명주기가 더 길기 때문에, 데이터를 보존할 수 있게 되는 것이다.

이렇게 관리한다면?

  1. UI와 데이터 로직을 분리시킴으로서 유지보수와 개발 효율을 높일 수 있다.
  2. 액티비티, 프래그먼트의 생명주기로부터 자유로워진다.
  3. 프래그먼트간 데이터 공유가 쉬워진다.

주의 ⛔️

데이터는 구성 변경과 같은 상황에만 데이터가 보존된다. 만약 사용자가 뒤로가기 버튼을 눌러 액티비티가 완전히 사용하지 않는 상태가 된다면 ViewModel의 유일한 함수인 onCleared()를 호출하여 내부 데이터를 초기화하고, 데이터를 파괴한다.

ViewModel 의 필요성에 대해 어느정도 알아보았으니, 실습을 하면서 자세히 알아보도록 하자.


🙃 실습

우선, ViewModel 클래스를 상속받는 클래스를 생성한다.

class UserViewModel : ViewModel() {
    var userLiveData = MutableLiveData<User>()
    
    fun editData(user : User) {
        userLiveData.postValue(user)
    }
}

어려운 코드는 없다. User 데이터를 받으면 MutableLiveData의 데이터를 갱신하는 코드이다. 기존에는 이 코드가 액티비티에 있었지만, 액티비티에서 분리시켜 따로 생성한 것이다.

그렇다면, 액티비티에서는 이 ViewModel을 어떻게 사용할까?

class EditProfileActivity : AppCompatActivity() {
    // val viewModel : UserViewModel by viewModels() :: Kotlin property delegate
    private lateinit var viewModel : UserViewModel

    private val binding: ActivityEditProfileBinding by lazy {
        ActivityEditProfileBinding.inflate(layoutInflater)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
        viewModel = ViewModelProvider(this).get(UserViewModel::class.java)

        viewModel.userLiveData.observe(this) {
            binding.tvNickname.text = it.nickname
            binding.tvName.text = it.name
            Toast.makeText(this, "변경이 감지되었습니다.", Toast.LENGTH_SHORT).show();
        }

        binding.btnEdit.setOnClickListener {
            val setNickName = binding.edtNickname.text.toString()
            val setName = binding.edtName.text.toString()

            val userData = User(setNickName, setName)
            viewModel.editData(userData)
        }
    }
}

LiveData를 설명할 때 있던 코드를 기반으로 ViewModel을 사용해보았다. 크게 달라진 것은 없다. 기존 LiveData를 액티비티에서 그대로 사용했다면, ViewModel이라는 다리를 한 단계 거쳐 사용하는 것이다.

  // val viewModel : UserViewModel by viewModels() :: Kotlin property delegate
  private lateinit var viewModel : UserViewModel
  override fun onCreate(savedInstanceState: Bundle?) {
      viewModel = ViewModelProvider(this@EditProfileActivity).get(UserViewModel::class.java)
  }

선언은 위와 같은 형태로 진행된다. 방법은 두 가지가 있다.

  1. kotlin의 by 키워드, 즉 위임을 이용하여 초기화하는 방법, 이 경우 해당 ViewModel초기화되는 액티비티, 혹은 프래그먼트의 생명주기에 완전히 종속된다.
  2. ViewModelProvier를 이용하여 초기화하는 방법, 이 경우 액티비티 혹은 프래그먼트를 지정할 수 있고, 지정한 컴포넌트의 생명주기에 종속된다.

보통은 1번의 방법을 많이 사용하지만, 프래그먼트와 프래그먼트 사이에서 데이터 공유를 해야하는 상황이 나올 때, 2번을 사용한다.
프래그먼트는 액티비티에 종속이 되어있는 것을 생각해보면, 왜 2번을 써야하는지 알 수 있을 것이다. 이해가 되지 않는다면, 아래로 내려가보자.

위의 코드를 적용 후 실행하면, 아래와 같은 결과를 얻을 수 있다.


🥸 이 구간부터 '조금' 깊숙한 구간

ViewModelProvider

그렇다면 ViewModelProvider 는 뭘까? 다시 코드를 살펴보자.

    viewModel = ViewModelProvider(this@EditProfileActivity).get(UserViewModel::class.java)

직관적으로 코드를 보면 매개변수로 현재 액티비티를 주고, UserViewModel::class.java 를 이용하여 ViewModel 을 가져오는 것인데, 이 과정이 어떻게 이뤄지는지 조금만 더 알아보도록 하자.

기본적으로 ViewModel추상클래스로, 프로그래머가 직접 생성자 메소드로 인스턴스 생성이 불가능하다. (new 키워드를 사용할 수 없다는 의미) 그렇기 때문에, ViewModelProvider를 통해 ViewModel을 생성 및 요청하는 방법밖에 없다. 그리고 이 ViewModelProvider의 매개변수로 ViewModelStoreOwnerViewModelProvider.Factory 가 들어간다.

위의 코드에서는 ViewModelProvider 의 여러 생성자중 하나로, ViewModelStoreOwner 만 매개변수로 넣는 방법을 사용했다. ViewModelProvider.Factory를 매개변수로 넣는다면 코드는 아래와 같이 작성할 수 있다.

    viewModel = ViewModelProvider(this@EditProfileActivity,ViewModelProvider.NewInstanceFactory()).get(UserViewModel::class.java)

ViewModelStoreOwner?

이름만 보아도 알 수 있듯이, ViewModelStore의 주인이라는 뜻이다. 위의 코드에서는 EditProfileActivity가 될 것이다. ViewModelStoreOwnerViewModelStore를 보유하고 있는데, 이 ViewModelStore에서 ViewModel 객체가 HashMap 구조로 저장되어 있고, 여기서 ViewModel을 가져오는, 즉 get() 해오는 것이다.

🧐 여기서 더 생각해보면, 프래그먼트의 경우 액티비티에 종속되어 있는 형태라고 위에서 언급했다. 그렇다면 프래그먼트에서 ViewModel을 사용할 때 부모 액티비티를 ViewModelStoreOwner로 선언한다면 어떻게 될까? 여러 프래그먼트가 하나의 ViewModel을 공유할 수 있게 되는 것이다.

ViewModelProvider.Factory?

ViewModelProvider에 속해있는 인터페이스의 하나로,ViewModel 인스턴스를 생성한다. 위의 ViewModelStore에 접근하여 ViewModel Class, 위의 코드에서는 UserViewModel::class.java존재하는지 확인한다. ViewModel이 존재하지 않을 경우, 생성 후 ViewModelStore에 저장한다. 위의 코드에서는 안드로이드에서 기본적으로 제공하는 팩토리 클래스인 NewInstanceFactory() 를 사용하였다.

이 흐름을 그림으로 정리하자면 아래와 같다.

🖐 참고 : ViewModel은 액티비티 혹은 프래그먼트 내에서 1개만 생성이 가능하다. 즉, 액티비티 내의 싱글톤 객체로, 아무리 여러번 해당 ViewModel을 생성해도 하나의 객체만 계속 사용된다는 의미이다. 이해가 안된다면 아래 코드를 참고하자.

        private lateinit var viewModel : UserViewModel
        private lateinit var twoViewModel : UserViewModel // 의미 없음

        viewModel = ViewModelProvider(this,ViewModelProvider.NewInstanceFactory()).get(UserViewModel::class.java)
        twoViewModel = ViewModelProvider(this,ViewModelProvider.NewInstanceFactory()).get(UserViewModel::class.java) // 의미 없음

AndroidViewModel

기본적으로 ViewModel은 액티비티나 프래그먼트에 의존적인데, 개발을 하다보면 이를 넘어 Context를 사용해야 할 때가 있다. 그렇다고 이럴 때 액티비티나 프래그먼트를 참조하면 메모리 누수 현상(Memory Leak)이 발생한다.

위에서 이야기한 것을 생각해보자. 기본적으로 액티비티와 프래그먼트보다 긴 생명주기를 가지고 있는데, 여기서 액티비티에 대한 참조를 가지고 있다면, 액티비티가 아무리 사라져도 ViewModel은 사라지지 않고 메모리를 차지하고 있을 것이다. 그렇다면 범위를 좀 더 넓힐 필요가 있다.

그러기 위해서 AndroidViewModel을 사용하는데, 이 클래스는 Application에 의존적이다. 즉, 특정 액티비티나 프래그먼트가 사라지더라도 인스턴스가 유지되고, Application이 종료되는 시점에 onCleared()가 호출되어 데이터가 사라지게 된다.

// in ViewModel Class
class UserViewModel(application: Application) : AndroidViewModel(application) {
    var userLiveData = MutableLiveData<User>()
}

// in Activity or Fragment
    viewModel = ViewModelProvider(this,ViewModelProvider.AndroidViewModelFactory(application)).get(UserViewModel::class.java)

MVVM ViewModel? AAC ViewModel?

정말 놀랍게도 두 개는 연관이 전혀 없다. MVVM에 대한 학습이 되어있지 않다면 조금 이해가 어려울 수 있다.

  • MVVM ViewModel : View, Model 사이에서 데이터를 관리하고 바인딩하는 역할을 맡는다.
  • AAC ViewModel : 화면 전환과 같은 구성변경에도 데이터를 보존한다. 즉, 생명주기에 따른 편리한 데이터 관리가 목적이다.

그렇다고 AAC ViewModel을 MVVM ViewModel을 사용할 수 없는걸까?

당연히 아니다. 오히려 더 좋다. 위에서 이야기했듯 안드로이드의 구성변경에 민감하게 반응할 수 있는 것이니 AAC ViewModel을 MVVM화 시킨다면 안드로이드 개발자 입장에서는 너무나도 좋을 수 밖에 없다. 다만, AAC ViewModel 하나로는 MVVM ViewModel을 구현할 수 없다는 것이다.

한 가지 예로, MVVM의 경우 View와 ViewModel의 관계가 1:N으로 구성이 되어있지만, AAC ViewModel의 경우 액티비티 내에서 1개만 생성이 가능하다. (이해가 안된다면 위의 글 참고) 그렇다고 해서 ViewModel 자체를 여러번 생성하지 못하는 것은 아니다.

UserInfoActivity 가 존재한다면 UserNameViewModel, UserPhoneViewModel과 같이 여러 ViewModel로 나눠서 사용이 가능한 것은 맞으나, 구글은 하나의 View에 하나의 ViewModel만 두고 사용할 것을 권장하고 있다. 즉, UserInfoActivityUserInfoViewModel을 사용하고, 그 안에 여러 데이터를 넣어서 관리하라는 이야기이다. 이것은 MVVM 원칙에 맞지 않는다.

최종적으로 안드로이드에서 MVVM을 완성하려면, AAC의 LiveData, DataBinding, ViewModel등 여러가지를 조합해야 완성이 되는 것이다.

다시 강조하자면, MVVM ViewModel != AAC ViewModel


참고 및 출처

안드로이드 공식문서
날고싶은 개발자
오늘의 코드
쾌락코딩
ViewModel이란 무엇인가?
AAC ViewModel 을 생성하는 6가지 방법 - ViewModelProvider

profile
즐겁게 하자 🤭

0개의 댓글