[Android, Kotlin] Architecture Pattern - MVVM
Android

[Android, Kotlin] Architecture Pattern - MVVM

MVVM 아키텍처 패턴에 대해 알아본다.

이전 글에서 MVP 패턴의 경우 View와 Model이 분리된다는 장점이 있으나, 이에 따라 View와 Presenter 간 의존도가 높아지고, View 하나에 필연적으로 Presenter 하나가 따라오는 1:1 구조를 이루고 있어 앱의 사이즈가 커질 수록 코드의 양이 방대해진다고 하였다.

 

 

MVP 패턴 : https://hungseong.tistory.com/11

 

Architecture Pattern - MVP

넘 어려운 아키텍처 패턴 MVC는 초기 앱 만들 때 맨날 쓰던 패턴이라 익숙하다. 하지만 MVP와 MVVM 이 녀석들은 아무리 검색하고 공부해도 헷갈려서 이해가 안 간다. 대체 뭐가 뭔지 1도 모르겠다.

hungseong.tistory.com

 

 

그렇다면 MVVM은 무엇이 다를까?

MVVM은 Model , View , ViewModel로 구성된 아키텍처 패턴이다.

MVP에서 Presenter의 역할을 MVVM에서는 ViewModel이 담당하나, MVP와의 가장 큰 차이점은 View는 ViewModel에 의존하나 ViewModel은 View에 의존하지 않는다는 것이다.

이 역시도 그림으로 보는 것이 가장 편할 것 같다.

 

 

 

MVC와 MVVM의 차이를 설명하는 그림이다.

MVP와 MVVM을 비교하는 그림으로 구하고자 했으나, 이 그림이 너무 비유가 찰떡이라 가져왔다.

MVC의 경우, Controller에 해당하는 MainActivity가 View와 Model을 모두 참조하여 앱이 구성된다.

사실상 MainActivity가 V와C를 모두 담당한다고 보는 게 맞을 지도 모르겠다.

그러나 오른쪽에서 보듯 MVVM의 경우, View와 ViewModel이 상호 의존성을 가지고 있지 않다.

View에서 사용자의 click 등의 액션이 들어올 경우, 참조하는 ViewModel에 데이터를 요청한다. ViewModel은 UseCase를 중간 통로로 하여 Data Layer에서 데이터를 받아온다. 여기까지는 MVP와 같다.

그렇다면 ViewModel은 어떻게 View를 참조하지 않고 데이터의 변경을 전달할까?

위 그림에서 View가 말하듯 "바뀌는 것을 지켜보고" 있는다.

ViewModel에서 Live Data와 같이 observing이 가능한 값을 설정하고, View에서는 ViewModel의 LiveData를 구독한다.

Live Data를 통해 ViewModel이 가공해 온(변경한) 데이터를 View에서 받아 UI처리를 수행한다.

즉 , Live Data와 같은 observing 방식을 통해 ViewModel은 View를 참조하지 않고도 View에게 데이터를 전달해줄 수 있는 것이고, View는 Live Data를 구독(observing)함으로써 값의 변경을 알아채고 UI를 변경 할 수 있는 것이다.

아래 사진은 구글의 아키텍처 가이드이다.

 

 

 

 

화살표는 의존성을 의미한다.

보이는 것 처럼 화살표가 양방향으로 왔다갔다 하는 상호 의존성이 없이, 단방향으로 존재한다.

이렇게 상호 의존성이 존재하지 않는 방식이 MVVM 패턴이다.

 

Example
장바구니에 담긴 메뉴 리스트 불러오기

 

참고 - 본 예제에서는 UseCase를 사용하지 않고 ViewModel -> Repository의 상호작용으로 구성되었다.

 

 

class HomeViewModel(
    private val mapRepository: MapRepository,
    private val userRepository: UserRepository,
    private val restaurantFoodRepository: RestaurantFoodRepository
) : BaseViewModel() {

    //Live Data
    private val _foodMenuBasketLiveData 
    = MutableLiveData<List<RestaurantFoodEntity>>()

    val foodMenuBasketLiveData: LiveData<List<RestaurantFoodEntity>>
       get() = _foodMenuBasketLiveData

    fun checkMyBasket() = viewModelScope.launch {
        foodMenuBasketLiveData.value = 
            restaurantFoodRepository.getAllFoodMenuListInBasket()
    }

...

 

HomeViewModel에서 장바구니에 담긴 음식 List를 observing 할 수 있는 Live Data를 위와 같이 만든다.

하나의 프로퍼티만 만든 것이 아닌, _가 들어간 MutableLiveData를 만들고, get을 통해 _가 없는 LiveData가 따로 존재하는데, 이는 Backing Property라고 한다.

_가 있는 MutableLiveData 타입의 프로퍼티는 Write/Read 모두 가능하며,

_가 없는 LiveData 타입의 프로퍼티는 Read만 가능하다.

View에서는 _가 없는 LiveData 타입의 프로퍼티를 observing 하게 하고,

ViewModel은 _가 있는 MutableLiveData 타입의 프로퍼티 값을 수정함으로써 ViewModel에서만 값을 변경 할 수 있도록 한 것이다.

본 Backing Property는 무조건 사용한다고 좋은 것이 아니다. 간혹 View에서 LiveData를 변경하는 로직이 필요한 경우에는 Backing Property를 쓰지 않는 것이 맞다. 본 예제에서는 View에서 LiveData의 변경을 수행하지 않으므로 Backing Property를 사용하였다.

checkMyBasket 메소드를 통해 장바구니에 담긴 리스트를 Repository에서 가져오고, 그에 따른 결과를 받아와LiveData의 상태를 변경하여 View에 알린다.

이 때, LiveData를 사용함으로써 ViewModel에서 View를 참조하지 않는 것을 확인 할 수 있다.

 

 

class DefaultRestaurantFoodRepository(
    private val foodApiService: FoodApiService,
    private val foodMenuBasketDao: FoodMenuBasketDao,
    private val ioDispatcher: CoroutineDispatcher
): RestaurantFoodRepository {

    override suspend fun getAllFoodMenuListInBasket(): List<RestaurantFoodEntity> 
      = withContext(ioDispatcher) {
          foodMenuBasketDao.getAll()
    }
...

 

Repository의 getAllFoodMenuListInBasket 메소드에서는 Room의 Dao를 통해 장바구니에 담긴 음식 List를 가져오게 된다.

 

 

viewModel.foodMenuBasketLiveData.observe(viewLifecycleOwner) {
            if (it.isNotEmpty()) {
                binding.basketButtonContainer.isVisible = true
                binding.basketCountTextView.text = 
                getString(R.string.basket_count, it.size)
                binding.basketButton.setOnClickListener {
                    if (firebaseAuth.currentUser == null) {
                        alertLoginNeed {
                            (requireActivity() as MainActivity).goToTab
                            (MainTabMenu.MY)
                        }
                    } else {
                        startActivity(
                            OrderMenuListActivity.newIntent(requireActivity())
                        )
                    }
                }
            } else {
                binding.basketButtonContainer.isGone = true
                binding.basketButton.setOnClickListener(null)
            }
        }

 

위 코드는 HomeFragment에서 LiveData를 observing하는 구문이다.

viewModel의 LiveData를 구독하여, ViewModel에서 가공하여 LiveData에 넘겨준 값을 받아 그 값에 따른 UI 변경을 수행하는 것을 확인 할 수 있다.

observing 시에, 해당 코드 블록은 LiveData의 값이 변경 될 때마다 호출된다.

* observe하는 메소드는 Fragment에서는 onViewCreated 시점에서 수행하도록 해야 observing 동작의 중복을 막을 수 있다. 또한, observing하는 owner의 경우, Fragment에서는 this가 아닌 viewLifeCyclerOwner로 설정해야 Fragment의 정상적인 Life View Cycle에 맞춰 LiveData의 Cycle이 설정된다.

 

참고 : https://youtu.be/pErTyQpA390 

MVVM의 단점은 무엇인가요

 

위 코드가 단순한 예제라 쉬워 보일 수도 있지만, 사이즈가 큰 프로젝트 개발 시에, ViewModel은 여러 observing 객체를 보유하고 있어야하며, 직접적으로 view와 view model이 의존성을 갖는 것이 아닌, observing을 통한 방식으로 구동되기 때문에 View Model의 설계가 어렵다는 단점이 있다.

간단한 앱을 개발하는 데에 MVVM 패턴을 사용할 경우, 괜히 코드가 복잡해지고 어려워지게 된다.

그러므로, 언제나 MVVM이 좋다! 라는 마인드보다는 만들고자 하는 앱의 사이즈나 설계 방식에 따라 맞는 아키텍처 패턴을 설계하는 것이 최선이라고 생각한다.

 

 

 

 

출처 : https://developer.android.com/jetpack/guide

 

앱 아키텍처 가이드  |  Android 개발자  |  Android Developers

앱 아키텍처 가이드 이 가이드에는 고품질의 강력한 앱을 빌드하기 위한 권장사항 및 권장 아키텍처가 포함되어 있습니다. 이 페이지는 Android 프레임워크 기본을 잘 아는 사용자를 대상으로 합

developer.android.com

 

https://aonee.tistory.com/48

 

[Android] MVVM 패턴 적용해보며 배우기(1) - ACC, MVC와 MVVM비교, MVVM 장점

🔥목차🔥 🍓 안드로이드 아키텍처 컴포넌트 (AAC) 🍓 MVC 와 MVVM 차이점 🍓 MVVM 패턴 🍓 MVVM 장점 🐥 실습 - MVVM 패턴, Repository, BataBinding 적용 이어지는 시리즈 👉 [Android] MVVM 패턴 적용해보며

aonee.tistory.com