[안드로이드 공식문서 파헤치기] ViewModel의 모든 것!

dada·2021년 12월 19일
5
post-thumbnail

✅공부 배경

안드로이드 권장 아키텍쳐인 MVVM을 프로젝트에 적용하면서 AAC의 ViewModel에 대한 이해도가 깊어야함을 느끼게 되었습니다! ViewModel에 대해 더 깊이 공부해봅시다~

✅ViewModel은 왜 등장했을까?

✔ViewModel 등장 배경

  • 어떤 기술이 등장하기까지 선배 개발자 분들은 개발->어떤 부분에서 문제발생->문제 해결방법 고민->해결을 위한 기술 등장! 의 과정을 거칠거라고 생각합니다. 그렇다면 ViewModel은 어떤 문제로부터 시작된 기술일까요?

  • 사실 ViewModel은 MVVM이라는 소프트웨어 개발 아키텍쳐의 구성요소로서 존재했습니다. 즉 ViewModel은 반드시 안드로이드 개발에서만 사용되는 개념이 아닙니다!

  • 구글은 원래 MVVM 아키텍쳐 패턴의 구성 요소로 사용되던 ViewModel을 'MVVM패턴을 적용한 안드로이드 개발에 적용하기 쉽게 라이브러리화해서 제공하고, 이 라이브러리가 바로 AAC에 속해있는 ViewModel 입니다.

✔ViewModel이 존재하지 않는다면?

🧨생길 수 있는 문제 1

    class MainActivity : AppCompatActivity() {
            lateinit var count: TextView
            lateinit var button: TextView
            private var num = 0

            override fun onCreate(savedInstanceState: Bundle?) {
                super.onCreate(savedInstanceState)
                setContentView(R.layout.activity_main)

                count = findViewById(R.id.count)
                button = findViewById(R.id.button)

                button.setOnClickListener {
                    num++
                    count.text = num.toString()
                }
            }
        }
  • 위 코드는 카운팅하는 앱을 만들기 위한 코드입니다. 이 화면을 사진처럼 가로로 돌린다면 카운트가 4까지 되어있던 화면이 다시 0으로 초기화되어버립니다! 즉 화면의 생명주기가 변하면'데이터가 유지되지 않는다'는 문제가 있습니다

  • 이러한 이유가 발생하는 원인은, 세로로 있던 Activity인스턴스가 가로 모드로 바뀌면서 Activity의 수명주기 콜백 매소드가 onDestroy(가로모드로 변함)->onCreate(다시 세로모드)->onResume(세로모드로 사용자와 상호작용) 순서대로 호출되기 때문입니다.

  • onDestroy메소드가 호출될 때 Activity의 인스턴스가 메모리에서 소멸되고 다시 세로모드로 변경하면서 새로운 Activity인스턴스가 생성되면서 클래스의 멤버변수로 존재하고 1씩 증가하던 num변수가 Activity인스턴스 소멸과 함께 같이 사라지게 되는거죠! 즉 가로모드->세로모드가 되면서 새롭게 생성된 인스턴스와 함께 num변수로 새로 생성되어 초기값인 0으로 돌아오게 됩니다.

Activity와 같은 인스턴스가 onDestroy 호출과 함께 소멸된 후 다시 onCreate 가 호출되며 인스턴스로 새로 생성될 때 데이터가 유지되지 않고 초기화 되어버린다는 문제가 있다.

🧨아니~ onSaveInstanceState() 사용하면 되잖아요?!

  • 맞아요!! UI데이터(사용자의 눈에 보이고 화면에 보여야하는 데이터)가 단순한 데이터일 경우에는 Activity 클래스에서 제공하는 onSaveInstanceState()라는 콜백 메소드를 사용할 수 있습니다!
  • onSaveInstanceState()는 UI컨트롤러(Activity,Fragment)가 다시 생성될 때 호출되는 onCreate 콜백 메소드에서 인자인 bundle로 부터 데이터를 복원할 수 있도록 합니다
  • 하지만 onSaceInstanceState() 콜백 메소드는 list형태의 데이터처럼 대용량 데이터를 저장하기엔 무리가 있고, 직렬화후 다시 역직렬화 할 수 있는 소량의 데이터에게 적합한 방법입니다!

    대량의 UI데이터를 복원하기 위해선 ViewModel을 사용해야 합니다

🧨생길 수 있는 문제 2

  • 서버와 통신하는 데이터를 작성할 경우 UI컨트롤러인 Activity와 Fragment에 API를 call하는 코드를 작성해야 합니다. 그런데 UI 컨트롤러가 DB나 서버에 접근하는 코드를 가지고 있으면 UI컨트롤러에 과도한 책임이 가해지게 됩니다

  • UI컨트롤러는 말 그대로 UI 데이터를 갱신하거나 event를 trigger하는 역할만 해야합니다. 따라서 UI컨트롤러에 API데이터를 접근하는 코드를 넣지 않는게 좋습니다. 만약 UI컨트롤러에 이런 코드가 몽땅 적혀있으면 UI컨트롤러 혼자 앱의 모든 로직을 처리해야하니 유지보수 할때도 머리가 아플겁니다 ㅠㅠ

서버, 데이터베이스에 접근하는 코드를 UI컨트롤러와 분리하기 위해 ViewModel을 사용해야 합니다

✅AAC의 ViewModel

✔AAC의 ViewModel

  • 본 포스팅에선 AAC의 ViewModel에 대해 알아볼 것입니다! 아키텍쳐 관점에서의 ViewModel이 아닌 AAC로 제공되는 라이브러리인 ViewModel에 대한 내용을 다룰겁니다! 둘의 차이가 궁금하신 분들은 MVVM ViewModel과 AAC ViewModel의 차이를 참고해주세요

✔ViewModel이 해야하는 것

  • 앞서 살펴본 것처럼 기존에는 Activity 클래스 안에서 관리하던 UI데이터를 앞으로는 ViewModel 클래스 안에 저장하고 기존 UI컨트롤러 안에 서버,DB로 부터 데이터를 가져오는 로직, 데이터를 가공하는 로직을 처리하게 됩니다.

  • 이런 역할을 하는 ViewModel이 등장한 이유 안드로이드 앱 개발 원칙인 관심사 분리도 달성할 수 있게 됩니다.

    UI컨트롤러와 데이터 호출,가공 로직 즉 관심사를 분리시킨다(UI 컨트롤러인 Activity, Fragment는 수동적으로 UI를 그리고 사용자로부터 받은 event를 Trigger하는 역할만 하도록 한다.)

✅ViewModel 어떻게 생겼나?

  • 구글은 AAC ViewModel라이브러리를 ViewModel이라는 클래스를 통해 제공합니다.

  • ViewModel클래스는 Activity나 Fragment의 수명주기를 고려해 UI관련 데이터를 저장/관리 할 수 있도록하는 클래스 입니다.
    03

  • ViewModel는 object클래스를 상속하는 추상 클래스 입니다

  • ViewModel 클래스의 생성자는 ViewModel()이며 이 클래스에 구현된 메소드는 onCleared() 콜백 메소드 하나뿐입니다. onClear는 ViewModel 인스턴스가 메모리에 소멸되는 순간=더이상 인스턴스가 사용되지 않는 순간에 호출되기 떄문에 만약 ViewModel 인스턴스를 더 이상 사용할 필요가 없다고 판단되면 onCleared() 메소드 안에서 실행하던 작업을 끝내는 코드를 작성하면 됩니다(ex.채팅 데이터를 가져오기 위해 계속 flow로 데이터를 가지고 오고 있었다면 onCleared()해서 이 작업을 해제시켜준다)

✅ViewModel 구현해보자

✔ViewModel 라이브러리 종속성 추가

  • Google이 제공하는 AAC ViewModel 라이브러리를 프로젝트에 추가해야 ViewModel 클래스를 사용할 수 있습니다

  • 해당 링크를 참고하여 ViewModel 라이브러리를 프로젝트에 추가해 주세요.

  • 그리고! Google Maven 저장소를 프로젝트에 추가해야 Google Maven 저장소에 저장되어 있는 ViewModel 라이브러리를 가져올 수 있습니다.

    • // build.gradle 파일(프로젝트 수준)
      allprojects {
          repositories {
              google()
          }
      }
      
      // build.gradle 파일(앱 모듈 수준)
      dependencies {
          def lifecycle_version = "2.2.0" 
      
          implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
      }

✔ViewModel 클래스를 상속받은 클래스 만들기

  • 위 사진처럼 ViewModel클래스를 상속받은 MyViewModel클래스를 만들어주세요.
  • MyViewModel클래스를 만들었다면 해당 클래스가 관리해줄 UI데이터 변수를 만들어야 합니다.
  • UI데이터 변수는 AAC에서 제공하는 LiveData를 사용하도록 하겠습니다

✔UI데이터를 가져와 사용자에게 보여주는 코드 작성하기

  • LiveData에 Observe를 달아서 관찰 가능한 데이터로 만들면 setOnClickListener에서 이벤트 클릭 로직을안써도 라이브데이터가 업데이트 될때마다 observe에 의해 자동으로 감지되어 데이터 로직이 실행됩니다

✔ViewModelProvider

  • 위 사진에서 ViewModelProvider라는 클래스를 볼 수 있습니다. 이는 AAC ViewModel 라이브러리가 제공하는 또 다른 클래스 중 ViewModelProvider라는 클래스입니다. 이 클래스는 UI 컨트롤러에게 ViewModel 역할을 하는 클래스를 제공하여 UI 컨트롤러와 ViewModel 역할을 하는 클래스를 연결해주는 기능이 구현되어 있는 클래스입니다.
  • 따라서 ViewModelProvider 클래스에서 제공하는 기능을 사용하여 MainActivity와 MyViewModel을 연결해주고 MyViewModel을 클래스에 저장된 UI 데이터를 MainActivity 클래스로 가져와야 합니다.

  • ViewModelProvider 클래스가 제공하는 기능을 사용하기 위해서는 먼저 MainActivity 클래스 내에서 ViewModelProvider 클래스의 인스턴스(=객체)를 생성해야 합니다. ViewModelProvider 클래스의 생성자 함수를 호출하여 인스턴스를 생성할 수 있습니다.

  • 07
  • ViewModelProvider 클래스의 생성자 함수는 위와 같이 3가지 모습으로 존재합니다. default 생성자 함수인 첫 번째 함수를 호출하여 ViewModelProvider 클래스의 인스턴스를 생성해보겠습니다.(두 번째, 세 번째 생성자 함수를 사용해야 하는 경우도 있으니 상황에 맞는 생성자 함수를 선택해 호출하면 됩니다.) default 생성자 함수는 인자로 ViewModelStoreOwner라는 인터페이스의 구현체(인스턴스)를 전달해야 합니다.

  • 08
  • ViewModelStoreOwner은 AAC ViewModel 라이브러리가 제공하는 인터페이스 입니다. 이 인터페이스를 실제로 구현하고 있는 클래스는 많지만 눈여겨볼 클래스는 AppCompatActivity 클래스와 Fragment 클래스입니다.(ComponentActivity까지 추가로 눈여겨봅시다!)

  • ViewModelStoreOwner 인터페이스의 존재 목적은 ViewModelStore를 소유하고 있는 대상을 명시하기 위함입니다.(ViewModelStore에 대해서는 아래 내용에서 이어서 언급할 예정입니다.)

  • 11
  • ViewModelStoreOwner 인터페이스 내부에는 getViewModelStore() 라는 추상 메소드 하나가 정의되어 있습니다. 따라서 ViewModelStoreOwner 인터페이스를 구현하는 클래스들(Activity,Fragment,ComponentActivity)은 클래스 내부에 getViewModelStore() 함수를 오버라이딩하여 구현하고 있을 것입니다.

    • // ComponentActivity 클래스 내부에 구현되어 있는 getViewModelStore() 함수
      // Java 코드
      
      /**
      * Returns the {@link ViewModelStore} associated with this activity
      *
      * @return a {@code ViewModelStore}
      * @throws IllegalStateException if called before the Activity is attached to the Application
      * instance i.e., before onCreate()
      */
      @NonNull
      @Override
      public ViewModelStore getViewModelStore() {
          if (getApplication() == null) {
              throw new IllegalStateException("Your activity is not yet attached to the "
                      + "Application instance. You can't request ViewModel before onCreate call.");
          }
          if (mViewModelStore == null) {
              NonConfigurationInstances nc =
                      (NonConfigurationInstances) getLastNonConfigurationInstance();
              if (nc != null) {
                  // Restore the ViewModelStore from NonConfigurationInstances
                  mViewModelStore = nc.viewModelStore;
              }
              if (mViewModelStore == null) {
                  mViewModelStore = new ViewModelStore();
              }
          }
          return mViewModelStore;
      }
  • MainActivity 클래스는 보통 AppCompatActivity 클래스를 상속하는데 AppCompatActivity 클래스가 상속하는 부모 클래스를 타고 타고 들어가면 ComponentActivity 라는 클래스가 존재합니다. 이 클래스 내부에 ViewModelStoreOwner 인터페이스에 정의되어 있는 getViewModelStore() 메소드가 실제로 구현되어 있습니다. 따라서 ComponentActivity 클래스를 상속하는 서브 클래스인 MainActivity 클래스에서도 getViewModelStore() 메소드를 바로 호출할 수 있습니다.

  • getViewModelStore() 메소드의 return 값을 보니 해당 Activity와 연관된 ViewModelStore 클래스의 인스턴스를 반환하고 있습니다.(예시로 ComponentActivity 클래스 내에 구현된 getViewModelStore() 함수를 보고 있기 때문에 Activity와 연관된 ViewModelStore 클래스의 인스턴스를 반환하고 있지만 Fragment 클래스 내에 구현된 getViewModelStore() 함수는 해당 프래그먼트와 관련된 ViewModelStore 클래스의 인스턴스를 반환할 것입니다.) 여기서 ViewModelStore 는 AAC ViewModel 라이브러리에서 제공하는 또 다른 클래스입니다. 이 클래스는 ViewModel 역할을 하는 클래스의 인스턴스들이 UI 컨트롤러의 수명 주기에 관계 없이 어딘가에 지속적으로 저장되도록 설계된 클래스입니다.

    • // ViewModelStore 클래스
      // Java 코드
      public class ViewModelStore {
          private final HashMap<String, ViewModel> mMap = new HashMap<>();
      
          final void put(String key, ViewModel viewModel) {
              ViewModel oldViewModel = mMap.put(key, viewModel);
              if (oldViewModel != null) {
                  oldViewModel.onCleared();
              }
          }
          final ViewModel get(String key) {
              return mMap.get(key);
          }
          /**
          *  Clears internal storage and notifies ViewModels that they are no longer used.
          */
          public final void clear() {
              for (ViewModel vm : mMap.values()) {
                  vm.onCleared();
              }
              mMap.clear();
          }
      }
  • ViewModelStore.java 클래스 코드를 직접 봐보면 HashMap 자료구조를 사용하여 HashMap 안에 key-value 쌍으로 ViewModel들의 인스턴스를 저장하고 있음을 확인할 수 있습니다. ViewModel 클래스를 상속하여 만든 ViewModel 역할을 하는 클래스들의 인스턴스가 ViewModelStore 클래스의 HashMap에 저장되는 것입니다. 따라서 이 ViewModelStore 클래스의 인스턴스가 메모리에 남아있는 한 UI 컨트롤러 인스턴스가 메모리에서 소멸되고 다시 생성되어도 해당 UI 컨트롤러와 연결된 ViewModel 인스턴스를 HashMap에서 다시 찾을 수 있는 것입니다.(즉, ViewModel 역할을 하는 클래스에 UI 데이터를 저장할 경우 UI 데이터가 소실되지 않습니다.)

  • 정리하자면 ViewModelStore를 누가 소유하고 있느냐? -> this가 소유하고 있다 = MainActivity가 소유하고 있다.그렇다면 ViewModelStore은 무엇일까?
    ViewModelStore은 ViewModel 객체가 HashMap 구조로 저장되는 곳입니다.
    즉, get() 안에 있는 'UserViewModel..'은 객체를 찾아오기 위한 Key값으로 쓰이는 것입니다

    그림출처 오늘의 코드

뷰 모델을 HashMap 구조로 저장하니까 get() 메서드에 Key값을 넣어준 거고.
(만약 Key에 해당하는 Value가 없으면 생성하고 가져온다. 그래서 처음 뷰 모델 객체를 처음 만드는데도 set 따위가 아니라 get을 쓰는 것)
저 ViewModelStore를 소유하고 있는 주체가 MainActivity라는 것을 알려주는 것입니다

이를 통해 우리는 2가지 사실을 알 수 있습니다
1. 뷰 모델을 각각 다른 소유자가 생성하면 이는 별개의 메모리 공간을 사용하는 다른 객체가 된다.
2. 하나의 액티비티를 소유자로 지정해 사용하면 같은 ViewModel을 공유할 수 있다. = 데이터 공유 가능

  • 이전에 MyViewModel 클래스가 저장하고 관리하는 UI 데이터를 LiveData라는 것으로 감싸 선언했었습니다. LiveData가 제공하는 observe() 라는 함수를 사용하여 MyViewModel 클래스에 저장되어 있는 UI 데이터의 값이 바뀌는지 바뀌지 않는지를 UI 컨트롤러에서 관찰할 수 있습니다. 즉, UI 컨트롤러에서 UI 데이터를 계속 관찰하고 있는 것입니다. 관찰하다가 UI 데이터의 값이 바뀌면 observe() 함수의 인자로 전달한 observer의 onChanged() 함수가 실행됩니다. 따라서 onChanged() 함수 내에 MainActivity의 TextView에 UI 데이터를 세팅하는 코드를 작성해주면 됩니다.

✅ ViewModel의 수명 주기

ViewModel도 수명 주기를 가지고 있다!

  • AAC ViewModel 라이브러리가 제공하는 ViewModel 클래스를 상속하여 MVVM 패턴의 ViewModel을 구현하면 UI 컨트롤러의 수명 주기에 관계 없이 UI 데이터를 유지할 수 있다고 했습니다.

  • UI 데이터를 유지할 수 있는 이유는 ViewModel 인스턴스가 살아있는 수명 주기가 UI 컨트롤러보다 길기 때문입니다. 위에서 ViewModel 인스턴스는 ViewModelStore 클래스의 HashMap 자료 구조 안에 저장됨을 알아보았습니다. 또한 ViewModelStore 인스턴스는 UI 컨트롤러 인스턴스가 메모리에서 소멸되어도 같이 사라지지 않기 때문에 UI 컨트롤러 인스턴스가 재생성될 때 메모리에 아직 살아있는 ViewModelStore 에서 해당 UI 컨트롤러와 연결된 ViewModel을 다시 가져옵니다.

  • 그럼 ViewModel 인스턴스는 메모리에 언제까지 남아있게 되는 걸까요??? ViewModel 인스턴스가 메모리에 남아 있는 기간을 ViewModel의 수명 주기라고 부릅니다.

  • ViewModel 인스턴스의 수명 주기는 ViewModelProvider를 통해 ViewModel 인스턴스를 가져올 때 ViewModelProvider 생성자 함수의 인자로 전달하는 ViewModelStoreOwner의 수명 주기를 따르게 됩니다.

    • 12
  • 만약 ViewModelProvider 생성자 함수로 전달된 ViewModelStoreOwner가 Activity였다면 해당 ViewModel 인스턴스 수명 주기는 Activity의 수명 주기가 완전히 끝날 때까지 입니다. Activity의 수명 주기가 완전히 끝나는 시점은 위 그림에서 볼 수 있듯이 Finished 상태가 된 시점입니다. 따라서 이 때의 ViewModel 인스턴스는 Activity가 완전히 끝나지 않으면 계속 메모리에 남아있게 됩니다.

  • 따라서 Activity의 수명 주기를 따르는 ViewModel 인스턴스는 Activity 화면이 가로로 회전하여 onPause() -> ~ -> onDestroy() -> onCreate() -> ~ -> onResume() 콜백 메소드가 호출되는 동안에도 메모리에 남아있기 때문에 ViewModel 클래스에 저장된 UI 데이터는 소실되지 않는 것입니다. onDestroy()에 의해 Acitivity의 인스턴스가 메모리에서 사라지고 다시 새로운 인스턴스로 생성되어 onCreate()가 호출되더라도 이 새로운 Activity의 인스턴스는 아직 남아있는 ViewModel 인스턴스에 재연결됩니다.

  • Activity에서 ViewModel 인스턴스를 가져오는 코드가 onCreate() 콜백 메소드 내에 구현되는 이유도 화면이 회전되어 새로운 인스턴스가 생성되면서 onCreate() 콜백 메소드가 다시 호출될 때 ViewModel을 재연결시켜주기 위함입니다.

- Fragment의 수명 주기를 따르는 ViewModel 인스턴스는 Fragment가 Activity에서 분리될 때까지(onDetach가 호출되면 Activity와 연결 끊어짐) 메모리에 살아있습니다.

  • 만약 ViewModelStoreOwner이 완전히 소멸될 경우(Activity의 경우에는 Finished 상태가 될 경우) Android 프레임워크는 ViewModel 인스턴스를 메모리에서 제거합니다. 이 때 ViewModelStore 클래스에서 제공하는 clear() 메소드가 자동으로 호출되어 ViewModel들을 저장하고 있는 HashMap을 비우고 ViewModel들에게 더 이상 자신들이 사용되지 않음을 알립니다.

ViewModel 역할을 하는 클래스는 Activity/Fragment의 뷰를 직접 참조해서는 안된다!

  • ViewModel 역할을 하는 클래스의 존재 목적은 UI 데이터를 저장하고 관리(UI 데이터를 외부로 넘길 수 있게 하거나 UI 데이터 값을 변경함)하는 것 뿐입니다.

  • 이 때 주의해야 할 점은 ViewModel 역할을 하는 클래스 내부에서 Activity/Fragment의 뷰 인스턴스를 직접 참조해서는 안 된다는 것입니다. 다시 말하면 ViewModel 역할을 하는 클래스는 자신이 어떤 Activity/Fragment와 연결되고 자신이 가지고 있는 UI 데이터가 어떤 뷰에 세팅되는지에 관한 코드가 절대 작성되면 안 된다는 것입니다.

  • 만약 ViewModel 역할을 하는 클래스 내부에서 Activity의 뷰 인스턴스를 직접 참조한다고 가정해봅시다. 이 때 해당 Activity의 화면을 가로로 회전하여 세로 Activity 인스턴스가 메모리에서 제거되었다면 어떻게 될까요???

  • 세로 Activity 인스턴스가 메모리에서 사라지더라도 ViewModel 인스턴스는 메모리에 그대로 남아있게 됩니다. 이렇게 아직 살아있는 ViewModel 역할을 하는 클래스는 이제 이미 사라지고 없는 Activity의 뷰 인스턴스를 계속 참조하게 됩니다. 이것이 바로 메모리 누수(Memory Leak) 현상 입니다.

참고)
https://choheeis.github.io/newblog//articles/2021-02/fragment
https://todaycode.tistory.com/33
https://blog.mindorks.com/android-viewmodels-under-the-hood
https://blog.mindorks.com/shared-viewmodel-in-android-shared-between-fragments
Android Developer 도큐먼트 - ViewModel 개요
Youtube 영상 - Android Jetpack : ViewModel

profile
'왜?'라는 물음을 해결하며 마지막 개념까지 공부합니다✍

0개의 댓글