본문 바로가기

Android(Kotlin) Study A부터 Z까지 등반

MVVM ViewModel vs AAC-ViewModel에 대해서

MVVM ViewModel vs AAC-ViewModel

이름만 같고 동작하는 방식이 전혀 다르다, AAC-ViewModel아키텍처적인 VM과는 완전 상관없다고 한다. 초기화 방식이 비슷해보이지만 그렇지 않다.

 

  1. MVVM 패턴의 ViewModel
    1. MVVM 패턴은 마틴 파울러의 Presentation 모델 패턴에서 파생된 디자인 패턴.
    2. ViewModel 사이의 매개체 역할을 하고 View에 보여지게 되는 데이터를 바인딩하고 가공하는 역할을 한다
    3. 비즈니스 로직과 프레젠테이션 로직을 UI로부터 분리하는 것을 목표로 하고 있음.
      • 프리젠테이션 로직 : 말 그대로 보여주기 위한 로직을 말한다. 즉 화면상의 디자인 구성을 위한 로직을 일컫는 말로써, 게시판에서의 표시하기 위한 for(or while)문 등의 사용이 여기에 해당한다.
      • 비즈니스 로직 : 어떠한 특정한 값을 얻기 위해 데이터의 처리를 수행하는 응용프로그램의 일부를 말한다. 즉 원하는 값을 얻기 위해서 백엔드에서 일어나는 각종 처리를 일컫는 말이다.
  2. AAC의 ViewModel
    1. 앱의 Lifecycle을 고려하여 UI 관련 데이터를 저장하고 관리하는 역할을 한다.
    2. 뷰모델 프로바이더로 초기화를 해야한다.

 💡 각각의 이름을 헷갈리지말자!

 

MVVM 패턴 이야기 하면서 구글 이야기를 한다면 자리를 박차고 나가라. - 강사룡님

MVVMAACViewModel과 연관성이 없다. AAC ViewModel을 전제로 MVVM을 설명하려고 한다면 단언컨데 우리 회사 1차 면접도 통과하지 못할 것이다.- 정승욱님

MVVM 패턴의 ViewModel의 구조, DataBinding 대신 LiveData 사용가능

  • 뷰에서 어떤 사용자 액션이 오면 ViewModel이 Model에게 데이터가 있는지 확인하고 Observer를 통해서 업데이트를 받고 VM은 View에 다시 알려준다.

결국에 VM을 쓰는 것은 View와 VM간의 관계를 단절 시키기 위함이다.

  • View와의 의존도는 Activity일수도 있고 Fragment일수도 있지만 DataBinding을 사용하면 의존도를 줄일 수 있다.

깔끔하게 분리한다면 팩토리 패턴이 만들어진다. 만약 로그인 버튼에 대해서 VM을 만들고 UI만 조금 바꿔주면 동작하는 것은 똑같지만 행동하는 것은 다르게 만들어 줄 수 있다. 로그인/로그아웃

결국에 리액티브 프로그래밍을 위해서 라이프싸이클이 필요하기 때문에 AAC-ViewModel을 필요로 한다고 한다.

💡 Rx는 모르겠지만 코루틴을 사용할 때 뷰모델의 생명주기로 launch할 수 있다. 일반적인 뷰모델에는 생명주기가 없기 때문에 AAC-ViewModel을 쓰는 것 같다.

 

AAC-ViewModel이란?

Activity와 fragment와 같은 UI 컨트롤러의 로직에서 데이터를 다루는 로직을 분리하기 위해 등장한 Android JetPack 라이브러리이다.

왜 두가지를 분리해야 할까?

  1. UI 컨트롤러의 목적 (액티비티나 프래그먼트 같은거 )따라서, UI 컨트롤러에서 데이터를 다루는 로직을 책임지게 되면 많은 유지보수가 필요한 비동기 호출과 같은 작업들을 해야하기 때문에 UI 컨트롤러에 과도한 책임이 생기게 된다. UI를 담당하는 메인 스레드에서 부하가 많아지면 끊김 현상도 생길수도 있음
  2. 데이터를 표시해주거나, 사용자가 어떤 작업을 했을때 반응을 보여주거나, 권한 요청과 같은 OS커뮤니케이션을 처리하는 것이 UI컨트롤러의 목적이다.
  3. 데이터 손실 방지 UI 컨트롤러에서 생명주기에 따라 앱이 활동중에 제거될 때마다 데이터를 저장시키고, 다시 생성될 때마다 데이터들을 다시 불러와줘야한다. 액티비티의 생명주기를 따라가면 유지하기가 어려움

💡 원래는 onSaveInstanceState()로 데이터 손실 방지를 했지만 작은 용량의 데이터에만 적합하고 데이터가 커지게 되면 적합하지 않다

 

더 자세하게 적은 문제

  • 담을 수 있는 데이터가 적다. 공식문서 - Parcelables and Bundles에서는 50k 미만의 데이터를 권장하고 있다.
  • 담을 수 있는 데이터의 형태가 제한된다.
  • onCreate에서 작업을 처리해야 하므로 UI 컨트롤러가 해야 할 일이 늘어나며 화면을 띄우는데 시간이 오래 걸린다.

💡 AAC ViewModel은 ViewModelProviders를 사용해서 ViewModel을 만드는데, 이렇게 만들어진 뷰모델은 그 액티비티에서 딱 하나만 존재하게 된다. 액티비티 한 개 내에서만 유효한 싱글톤인 셈이다. 이런 특성은 일반적인 MVVM에서는 강제되는 것이 아니기 때문에 혼란이 올 수 있다.

액티비티 한 개 내에서만 유효하다는 것이 AAC ViewModel을 액티비티 당 하나만 쓸 수 있다는 말은 아니다.

 

AAC-ViewModel의 정확한 목적, 거의 화면 회전할 때 쓰는 목적이라고 한다. 시스템에 의해 재생성 될때 데이터가 사라지는 것을 막기 위함

얘는 그냥 VM이라고 할 수 없고, 화면 회전에 대한 데이터를 쉽게 사용할 수 있게 만든것, 보통 VM할 때도 AAC-ViewModel을 쓰는 경우가 있다.

💡 근데 AAC-ViewModel은 처음부터 쓰지않고 나중에 쓰는 것이 좋다고 한다, 일단 프레젠트 만들듯이 접근하여 Dagger를 붙여서 VM을 쓴 후에 뷰모델을 초기화하는 방식이 가장 좋다고 한다. koin은 안된다. koin쓸 때는 AAC-VM을 초기화한다고 한다. why?

AAC-VM은 MSDN의 VM과는 상관 없다.

💡 MicroSoft Developer Network(MSDN) - MVVM 패턴 이건 통용되는 패턴(Android에서만이 아님) https://docs.microsoft.com/ko-kr/xamarin/xamarin-forms/enterprise-application-patterns/mvvm

 

💡 따라서 재생성 될 때 유지하고 싶은 데이터를 뷰모델에 저장한다.

 

재생성 될 때 유지해야하는 데이터는 무엇일까?

  • 보통 UI와 연관된 데이터이다.

근데 왜 MVVM 패턴을 검색하면 대부분 AAC ViewModel을 사용하는가?

💡 MVVM의 뷰모델역할은 모델 사이에서 데이터를 관리하고 바인딩해주는 것 뷰모델이 가지고 있는 데이터옵저버블하게 해주고, 뷰에서는 데이터 바인딩으로 그것을 구독하고 있으면 되는 것이다.

 

💡 AAC-ViewModel도 안될리가 없다. 오히려 화면 회전시에도 데이터를 유지시켜주기 때문에 더 좋다. 바인딩은 LiveData를 사용하여 바인딩해줄 수 있다.

 

뷰모델을 사용할 때 주의점

 💡 뷰모델에는 반드시 액티비티나 프래그먼트(Fragment), 컨텍스트(Context)에 대한 참조를 저장하면 안됩니다. 뷰(Views)도 컨텍스트를 가지기 때문에 뷰에 대한 참조 역시 저장하면 안됩니다. 화면 회전 시에는 종료된 컨텍스트 정보를 가지고 있기 때문(Memory Leak) ApplicationContext는 가지고 있어도 된다. AndroidViewModel() 사용

 

뷰모델 요청 로직

  1. ViewModelProvider를 통해 ViewModel 인스턴스를 요청한다.
  2. ViewModelProvider 내부에서는 ViewModelStoreOwner를 참조하여 ViewModelStore를 가져온다.
  3. ViewModelStore에게 이미 생성된(저장된) ViewModel 인스턴스를 요청한다.(이게 provider생성시에 매개변수로 Owner를 줬을 때 남아있는듯)
  4. 만약 ViewModelStore가 적합한 ViewModel 인스턴스를 가지고 있지 않다면,Factory를 통해 ViewModel인스턴스를 생성한다.
  5. 생성한 ViewModel 인스턴스를 ViewModeStore에 저장하고 만들어진 ViewModel 인스턴스를 클라이언트에게 반환한다.
  6. 똑같은 ViewModel 인스턴스 요청이 들어온다면, 1~3번의 과정을 반복하게 된다.

ViewModel 클래스를 상속하여 정의한 클래스는 개발자가 직접 생성자를 통하여서 인스턴스를 생성할 수 없고, ViewModelProvider.Factory 인터페이스를 필요로 한다.

ViewModelStoreOwner / ViewModelStore

  • ViewModel 은 ViewModelStore 라는 객체에서 관리를 한다.
  • ViewModelStore 클래스는 내부적으로 HashMap<String, ViewModel> 를 두어 ViewModel 을 관리.

ViewModelStore 객체는 누가 어떻게 만들고 관리할까?

💡 그건 바로 ViewModelStoreOwner 라는 녀석이 한다.

 

// Java Code
public interface ViewModelStoreOwner {
 
    @NonNull
    ViewModelStore getViewModelStore();
}

ViewModelStoreOwner 는 다음과 같이 생긴 인터페이스이고, FragmentActivity 의 부모격인 ComponentActivity와 Fragment 클래스가 이를 구현(Implement) 하고 있다. → 결국엔 생성할 때 Activity나 프래그먼트가 필요한것

public ViewModelProvider(@NonNull ViewModelStoreOwner owner) {
        this(owner.getViewModelStore(), owner instanceof HasDefaultViewModelProviderFactory
                ? ((HasDefaultViewModelProviderFactory) owner).getDefaultViewModelProviderFactory()
                : NewInstanceFactory.getInstance());
    }

public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {
        this(owner.getViewModelStore(), factory);
    }

public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
        mFactory = factory;
        mViewModelStore = store;
    }

그래서 어떻게 사용하는 것일까?

여기가 설명을 잘해줬다.

https://readystory.tistory.com/176

1. 파라미터가 없는 ViewModel - Lifecycle Extensions

dependencies {
    // ...
    implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
}
class MainActivity : AppCompatActivity() {
 
    private lateinit var noParamViewModel: NoParamViewModel
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
 
        /* use ViewModelProvider's constructor provided from lifecycle-extensions package */
        noParamViewModel = ViewModelProvider(this).get(NoParamViewModel::class.java)
    }
}
//this는 ViewModelStoreOwner 타입인데 액티비티, 프래그먼트 사용가능

2. 파라미터가 없는 ViewModel - ViewModelProvider.NewInstanceFactory

이는 안드로이드가 기본적으로 제공해주는 팩토리 클래스, ViewModelProvider.Factory 인터페이스를 구현하고 있음. 따라서 ViewModel 클래스가 파라미터를 필요로 하지 않거나, 특별히 팩토리를 커스텀 할 필요가 없는 상황에서는 1번 방법을 사용하거나, 2번 방법을 사용하면 되겠습니다. → 커스텀 안할거면 1번 쓰는게 나을듯

class MainActivity : AppCompatActivity() {
 
    private lateinit var noParamViewModel: NoParamViewModel
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
 
        noParamViewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())
            .get(NoParamViewModel::class.java)
    }
}

 

3. 파라미터가 없는 ViewModel - ViewModelProvider.Factory

  • 팩토리를 직접 구현하는 방법인데 장점은 하나의 팩토리로 다양한 ViewModel 클래스를 관리할 수도 있고, 원치 않는 상황에 대해서 컨트롤 할 수 있다.
class NoParamViewModelFactory : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return if (modelClass.isAssignableFrom(NoParamViewModel::class.java)) {
            NoParamViewModel() as T
        } else {
            throw IllegalArgumentException()
        }
    }
}

💡 위 코드는 NoParamViewModel 클래스가 아니면 IllegalArgumentException 을 던지도록 구현되어 있다. 이는 어디까지나 개발자의 마음대로 구현하면 되는 부분이며, 어떤 타입의 클래스가 전달되더라도 인스턴스를 생성하도록 구현할 수도 있다고함.

 

4. 파라미터가 있는 ViewModel - ViewModelProvider.Factory(3번의 연장선)

  • ViewModelProvider.Factory 를 구현하면 파라미터를 소유하고 있는 ViewModel 객체의 인스턴스를 생성할 수 있다. 직접 구현한 Factory 클래스에 파라미터를 넘겨주어 create() 내에서 인스턴스를 생성할 때 활용하면 된다.

그럼 이번에는 파라미터가 있는 ViewModel 을 정의하고 그에 대한 객체를 생성하는 예제를 작성해보자.

class HasParamViewModel(val param: String) : ViewModel()
class HasParamViewModelFactory(private val param: String) : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return if (modelClass.isAssignableFrom(HasParamViewModel::class.java)) {
            HasParamViewModel(param) as T
        } else {
            throw IllegalArgumentException()
        }
    }
}
class MainActivity : AppCompatActivity() {
 
    private lateinit var hasParamViewModel: HasParamViewModel
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
 
        val sampleParam = "Ready Story"
 
        hasParamViewModel = ViewModelProvider(this, HasParamViewModelFactory(sampleParam))
            .get(HasParamViewModel::class.java)
    }
}
//그냥 팩토리 생성시에 파라미터 넘기면 되는 것 같다.

5. 파라미터가 없는 AndroidViewModel - AndroidViewModelFactory

  • 사실 developer 사이트에 의하면 ViewModel 클래스에서 Context 객체를 소유하거나 접근하는 것에 있어서 권장하지 않고 있다. 하지만 정말 불가피하게 필요한 경우가 있을 수 있는데요. ViewModel 에서 Context 를 사용해야할 필요성이 있을 때는 AndroidViewModel 클래스를 사용하면 된다고 위에서 언급했다.
  • 안드로이드에서는 이러한 AndroidViewModel 객체에 대한 생성을 위해 ViewModelProvider.AndroidViewModelFactory 라는 별도의 팩토리를 제공한다. 예제를 통해 살펴보자.
class NoParamAndroidViewModel(application: Application) : AndroidViewModel(application)

AndroidViewModel 은 Application 객체를 필요로한다. 이번에는 AndroidViewModelFactory 를 이용하여 뷰모델 객체를 생성할 것이다.

class MainActivity : AppCompatActivity() {
 
    private lateinit var noParamAndroidViewModel: NoParamAndroidViewModel
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
 
        noParamAndroidViewModel = ViewModelProvider(this, ViewModelProvider.AndroidViewModelFactory(application))
            .get(NoParamAndroidViewModel::class.java)
    }
}//큰 차이는 없다.
//AndroidViewModelFactory는 내부에 NewInstanceFactory를 상속했다.

6. 파라미터가 있는 AndroidViewModel

  • 드디어 마지막이다. 파라미터가 있는 AndroidViewModel 객체를 생성하는 방법이다.
  • 사실 4번의 방법으로도 가능하다만, 이번에는 5번에서 살펴본 AndroidViewModelFactory 와 유사한 방식으로 커스텀 팩토리를 구현해보도록 하자.
  • 먼저 파라미터가 있는 AndroidViewModel 클래스를 준비한다.
class HasParamAndroidViewModel(application: Application, val param: String)
    : AndroidViewModel(application)
class HasParamAndroidViewModelFactory(private val application: Application, private val param: String)
    : ViewModelProvider.NewInstanceFactory() {//ViewModelProvider.Factory로 해도 된다고 한다.
 
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (AndroidViewModel::class.java.isAssignableFrom(modelClass)) {
            try {
                return modelClass.getConstructor(Application::class.java, String::class.java)
                    .newInstance(application, param)
            } catch (e: NoSuchMethodException) {
                throw RuntimeException("Cannot create an instance of $modelClass", e)
            } catch (e: IllegalAccessException) {
                throw RuntimeException("Cannot create an instance of $modelClass", e)
            } catch (e: InstantiationException) {
                throw RuntimeException("Cannot create an instance of $modelClass", e)
            } catch (e: InvocationTargetException) {
                throw RuntimeException("Cannot create an instance of $modelClass", e)
            }
        }
        return super.create(modelClass)
    }
}

💡 android-ktx / fragment-ktx 모듈을 사용하면 보다 편리하게 뷰모델 인스턴스를 생성할 수도 있다고 한다.

 

-출처-

https://speakerdeck.com/taehwandev/android-viewmodel?slide=4

https://wooooooak.github.io/android/2019/05/07/aac_viewmodel/