우린 안드로이드 MVVM을 사용한다 하더라도, 그냥 있는 그대로 사용하는 경우가 많다.
- 다른 회사에서도 사용하니까 대세라는 이유로 사용하는 경우
- 단순히 회사에서 선임들이 짜둔 코드를 익혀서 사용하는 경우
- 안드로이드 코드랩에 있는 코드만 보고 사용하는 경우
등이 있을 것이다. 이런 것들이 나쁘지는 않다. 이것들 만으로도 충분히 견고하고 유지보수가 가능한 앱을 만들지도 모른다. 하지만, 나는 단순히, 그저, 만들지도 모른다에 집중하고 싶다.
그래서 난, MVVM패턴이란 무엇인지, 데이터의 흐름은 어떻게 되는지, 왜 사용하는지, 현재 프로젝트엔 어떻게 적용할지 세세히 적어나갈 생각이다.
이 글을 읽는 사람들은 이 포스팅 하나만으로 MVVM이 무엇인지 정확히 이해하게 될거라 생각한다. 다른 포스팅들을 읽는다 하더라도, 이 포스팅이 여러분들의 MVVM의 개념을 잡는데 중점이 되도록 하는게 이번 포스팅의 핵심이다.
그럼 시작해보자!
안드로이드 MVVM이 무엇인지 이해하기 위해선 MVVM의 약자를 이해하면 매우 큰 도움이 된다고 생각한다. 그래서 난 하나하나 찾아보며 이해해 보았다. Model, View, ViewModel순서로 개념을 명확히 짚어보자
model. 뭔가 이 단어만으론 솔직히 감이 잘 잡히지 않는다. 그래서 네이버 사전을 찾아보았다.
4가지 정도의 뜻이 보인다. 난 그중, 2번의 뜻에 집중하고자 한다. 상품의 모델이나 디자인. 이를 프로그래밍 관점으로 보자면? 데이터의 모델이나 디자인으로 이해할 수 있지 않을까?
데이터의 모델이나 디자인은 뭘까? 간단히 생각해보자면, 데이터의 구조라고 금방 이해할 수 있을 것이다. 그렇다. 데이터의 구조. 다음과 같은 형태가 바로 데이터의 구조이다.
// 데이터 구조 예시 첫 번째
data class Person {
val name: String,
val age: Int,
val weight: Int,
val tall: Int,
}
// 데이터 구조 예시 두 번째
data class PersonRequestBody {
val token: String,
...
}
// 데이터 구조 예시 세 번째
data class PersonResponseBody {
val name: String,
...
}
즉, 위와 같이 짜여져 있는 데이터 구조를 사용하여 특정 '비즈니스로직을 수행'하는 컴포넌트들의 집합이 바로 '모델'이라 할 수 있는 것이다. 그리고 '비즈니스로직을 수행한다'의 뜻은 내부 DB나 서버에 데이터 CRUD(Create, Read, Update, Delete)를 수행하는 것을 의미한다. 이런 로직이 수행되는 컴포넌트들의 집합을 바로 '모델'이라 한다.
view는 뭘까? 난 안드로이드 개발자이니, 안드로이드 관점에서 해석해볼까 한다. view라고 하면 가장 먼저 떠오르는건, Activity/Fragment가 생각이 난다. 그리고 이와 매핑되는 view.xml이나 JetPack Compose가 생각이 난다.
이처럼 사용자에게 직접 보일 수 있고 상호작용 할 수 있는 컴포넌트가 바로 view인 것이다. 이곳은 사용자들에게 데이터를 보여주기도 하며, 사용자들로부터 데이터를 입력받기도 한다.
MVVM패턴의 핵심 컴포넌트라 할 수 있는 viewModel. 이 viewModel이란 이름은, 앞서 설명했던, view와 model의 이름을 모두 가지고 있다. 그렇다면 이렇게 스스로 생각해볼 수 있을것 같다.
"ViewModel...?
View와 Model이름을 모두 가지고 있으니...
이 둘을 이어주는 역할을 하는걸까...?"
난 위와 같이 생각했다. 즉 view와 model의 중계역할을 해주는 컴포넌트가 바로 viewModel인 것이다.
앞에서, View는 사용자들의 데이터를 입력받는 곳이라고 했다. 그리고 반대로, View는 사용자들에게 데이터를 보여준다고도 했다.
또한 Model을 통하여 내부DB나 서버에 데이터 CRUD를 수행한다고도 말했었다.
즉, ViewModel의 역할은 위의 기능을 모두 겸하는 것이다. View를 통해 데이터를 입력받게 된다면, 이를 ViewModel에서 수신한다. 또한 반대로 ViewModel에서 View에 데이터를 내보내줄 수도 있는 것이다.
같은 논리로, ViewModel을 통해 데이터 CRUD를 수행하기 위해 Model에 데이터를 보내주기도하며, 반대로 받기도 한다.
이렇게 ViewModel은 View와 Model을 중계해주는 역할을 해주는 것이다.
솔직히 위와 같은 데이터 흐름도를 보면 이런 생각이 들 수 있다.
"아니.., 왜 굳이 이렇게까지 복잡하게 나눠서 사용하는거지?"
솔직히 의문이 들었다. 그리고 이렇게까지 나눠놓을 필요는 없을것 같은데 말이다. 그래서 왜 안드로이드 MVVM을 사용하는지 이유를 알고싶어 찾아보던 도중, MVVM아키텍쳐패턴의 변천사를 공부하면 이해가 쉬울거라 판단했다. 그리고 공부해본 결과, 세 가지 키워드가 보였다. MVC, MVP, MVVM
여기서부터가 본격적인 시작이다.
시작하기 전, 위와 같이 데이터 흐름도를 그려보면 이해가 쉬울거라 생각한다.
빨간색으로 Activity/Fragment라고 표시해놓은 것처럼 MVC패턴에서 Controller == View == Activity/Fragment를 의미한다. 아주 쉽다. 즉, 거의 모든 코드를 Activity와 Fragment에다 때려박는다고 생각하면 된다.
물론 이 패턴이 초보자들도 접근하기 쉽고, 규모가 작은 앱을 만들때나, 빨리 만들어야만 하는 앱을 만들땐 괜찮을 순 있다. 하지만, 확장성을 생각하는 순간 재앙이 시작될 수 있다. 좀 더 자세히 말하자면, Activity와 Fragment에는 수천줄이 작성되면서 확장성과 유지보수에는 더욱 많은 시간과 비용이 들어간다.
물론, 이러한 MVC패턴을 사용한다 하더라도, 관심사를 잘 분리하고 나눠놓는다면 괜찮을 순 있다. 하지만 그럼에도 불구하고, MVC패턴의 대안책(MVP)이 나왔다는 건, 아무래도 Activity와 Fragment에 수천줄의 코드가 작성된다는건 커다란 불편함을 의미한다.
MVP패턴도 우선 데이터 흐름도를 먼저 보자.
위에서 보여줬던 MVC패턴과는 흐름이 조금 달라졌다. 그리고 각 컴포넌트들(Model, View, Presenter)이 가지고 있는 요소도 조금씩 바뀌었다. 크게 바뀐 부분이라하면, View와 Presenter부분이다.
우선, View의 경우는 Activity/Fragment와 그에 소속된 view.xml파일들, 그리고 이곳에 데이터를 통지해줄 수 있는 인터페이스(ex, interface View)을 의미한다.
그리고 Presenter의 경우는 사용자의 응답을 받기 위한 클래스와 인터페이스를 의미한다(ex, interface Presenter, class MainPresenter)
MVP의 경우, 이정도로는 설명이 부족할것 같아 코드를 첨부해볼까 한다.
class MainActivity extends AppCompatActivity implements MainContract.View {
private val mainPresenter: MainContract.Presenter by lazy {
MainPresenter(this)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mainPresenter.getUserList()
mainPresenter.getBrandList()
...
}
override fun showUserList(users: List<User>) {
// TODO users 데이터를 가지고 리사이클러뷰에 뿌려준다
}
override fun showUserList(brands: List<Brand>) {
// TODO users 데이터를 가지고 리사이클러뷰에 뿌려준다
}
}
class MainContract {
interface View {
fun showUserList(users: List<User>)
fun showBrandList(brands: List<Brand>)
...
}
interface Presenter {
fun getUserList()
fun getBrandList()
...
}
}
class MainPresenter(
private val view: MainContract.View
) implements MainContract.Presenter {
private val dataBase: DataBase by lazy { DataBase() }
override fun getUserList() {
val userList = dataBase.getUserList()
view.showUserList(userList)
}
override fun getBrandList() {
val brandList = dataBase.getUserList()
view.showBrandList(userList)
}
}
바로 위와 같은 패턴이 MVP패턴인 것이다. 동작 순서를 찬찬히 살펴보도록 하자.
MVP패턴 동작 순서
- View(=MainActivity)에서 사용자 정보를 불러오려고 한다. 그러면 프레젠터(=MainPresenter)를 통해서 사용자 정보를 요청한다. (=mainPresenter.getUserList())
- 프레젠터에선 모델(=DataBase)을 통해 사용자 데이터를 요청한다.(=database.getUserList)
- 프레젠터에서 이를 응답받는다.(val userList = database.getUserList()) 이제 이렇게 받은 데이터를 View에 통보해준다(view.showUserList(userList)
- View에선 이를 오버라이딩한 메소드안에서 응답받는다.
(= override fun showUserList(users: List) { ... }- 이렇게 받은 데이터를 통해(=users), View안에 있는 리사이클러뷰에 뿌려준다.
위와 같은 일련의 과정을 수행하는 아키텍쳐 패턴이 바로 MVP이다. 여기까지 읽어보고 이해가 잘 안된다면 여기서 읽기를 잠시 멈추고 위 코드와 순서를 찬찬히 살펴보길 권장한다.
그리고 이해가 되었다면 직관적으로 느꼈겠지만, 위 MVP패턴은 MVC패턴보다 관심사도 훨씬 분리가 되어 있다. 하지만 결국, 이러한 MVP패턴도 단점이 존재한다..
그것은, MVP패턴을 적용하고 앱이 커진다면, 위에서 말한 MainContract쪽의 코드가 비대해진다는 점이고 이로써 관리가 어려워진다는 점이었다. 좀 더 세부적으로 말하자면, 'MainContract.View'인터페이스와 'MainContract.Presenter'쪽의 코드가 점점 비대해지는 문제가 발생한 것이다.
또한 정말 큰 문제는 바로 View와 Presenter는 1:1관계를 가진다는 점이다. 이로써 프레젠터끼리 비슷한 코드가 생긴다 하더라도(ex. MainContract.Presenter와 DetailContract.Presenter끼리) 이들에 대한 중복관리를 하는데 있어 커다란 문제가 되었다. 중복코드가 있더라도 울며 겨자먹기식으로 코드를 추가해야만 했다.
개인적으로 이 부분이 와닿지 않아 좀 더 찾아보며 연구해보았다. 예를 들어보도록 하겠다. 프레젠터가 비대해져서 관리가 어려워 진다는건 무슨말일까?
class MainContract {
interface View {
fun showUserList(users: List<User>)
fun showBrandList(brands: List<Brand>)
...
}
interface Presenter {
fun getUserList()
fun getBrandList()
...
}
}
이는 일전에 정의헀던 프레젠터와 뷰이다. 위 모듈들은 오로지 'MainActivity'만을 위한 모듈이며, 오로지 MainActivity에만 종속된다. 다른 곳에서는 쓰일 수가 없다. 그러기에 재사용성도 매우 낮을 수밖에 없다.(왜일까? 만약, MainContract.Presenter의 getUserList만 재사용하고 싶어서 다른 액티비티에서 이를 오버라이딩하려 한다면...?MainContract.Presenter에 포함된 모든 인터페이스를 다시 구현해야만 할것이기 때문이다.)
즉, MainContract.View안에 있는 메소드를 재사용할 수 없다. 마찬가지로, MainContract.Presenter안에 있는 메소드도 재사용할 수가 없는 것이다.
그래서 무분별한 오버라이딩의 불상사를 겪을 바엔... 개발자들은 차라리 새로운 프레젠터에다가 단순히 코드를 추가하는 편이 더 나은 선택이었을 것이다.
class DetailContract {
interface View {
// 이녀석만 MainContract.View에서 재사용하고 싶지만 그럴수가 없다.
fun showUserList(users: List<User>)
fun showProductList(brands: List<Product>)
fun showOnBtn()
...
}
interface Presenter {
// 이녀석만 MainContract.Presenter에서 재사용하고 싶지만 그럴수가 없다.
fun getUserList()
fun getProductList()
fun subscribeOn(id: String)
...
}
}
그래서 MVP패턴은 UI와 비즈니스로직을 잘 분리했다 하더라도 '비즈니스 로직 자체에 대한 관심사를 한번 더 분리하지 못하는 큰 문제가 있는 것이었다. 그리고 바로 이러한 문제에 대항하기 위해 나온 아키텍쳐 패턴이 바로 MVVM이었다.
안드로이드 MVVM의 데이터 흐름도를 다시 한번 올려볼까 한다.
이 흐름도는 위에서 첨부한 흐름도와 딱 한가지 달라진 점이 있다. 바로, ViewModel에서 View로 이벤트를 응답할 때 (by LiveData)이라는 말이 추가됐다는 점이다. 이게 바로 MVVM의 핵심이다.
MVP의 경우는 MainContract.Present와 MainContract.View를 통해 Activity안에서 직접 데이터를 설정해주었다. 그리고 그럴 수밖에 없었다.
하지만, MVVM을 사용하면 LiveData형태의 데이터를 사용함으로써, Activity뿐만 아니라 view.xml안에서도 데이터를 설정해주는게 가능하다. 그리고 view.xml안에 데이터를 설정해주는 것을 바로'데이터 바인딩'이라고 한다. 이는 안드로이드 MVVM의 핵심이다. 코드로 한번 살펴보도록 하자.
class MainActivity extends AppCompatActivity {
private val mainViewModel: MainViewModel by viewModel()
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.activity = this
binding {
mainViewModel.getUserList()
mainViewModel.getBrandList()
...
}
}
class MainViewModel() : ViewModel() {
private val dataBase: DataBase by lazy { DataBase() }
private val _userList = MutableLiveData<MutableList<User>>()
val userList : LiveData<MutableList<User>>
get() = _userList
fun getUserList() {
val userList = dataBase.getUserList()
_userList.value = it.userList
}
}
class단의 코드는 이게 끝이다. 따로 Activity안에서 ui를 설정해주는 코드는 작성해주지 않아도 된다. 나는 개인적으로 이렇게 작성하는 것이 관심사도 더욱 잘 분리된거라 생각한다.
"그럼 ui에 데이터를 어떻게 set해주는거지?"
아래는 xml에서 데이터를 직접 바인딩해주는 모습이다. 기존에는 액티비티로에서 직접 데이터를 set해주었지만, dataBinding을 사용한다면 그러지 않아도 된다.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="mainVm"
type="com.example.main.MainViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvMain"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="@id/tvTitle"
app:layout_constraintBottom_toBottomOf="parent"
tools:setItems="@{mainVm.userList}" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
위 코드에서 tools:setItems="@{mainVm.userList}"이 부분이 주목할 부분이다. 이 부분을 통해서 관심사를 분리하고 있다. 그리고 이렇게 설정해줄 수 있는 부분이 바로 bindingAdapter라고 한다.
즉, 이렇게 정리할 수 있다.
dataBinding을 통하여 xml에 데이터를 설정해준다. 이때, 커스터마이징한 로직이 필요할 수 있는데, 이를 가능하게 해주는 것이 바로 binndingAdapter인 것
그렇다면 bindingAdapter코드를 보도록 하자.
@BindingAdapter("tools:setItems")
fun <T> RecyclerView.setItems(items: List<T>?) {
items?.let {
when (adapter) {
is MainAdapter -> {
(adapter as MainAdapter).loadUserList(items as List<User>)
}
}
}
}
이처럼 각 View에 대한 Extension을 따로 분리할 수 있는 것이다. 그리고 이러한 부분은 모든 View에도 적용이 가능하다. TextView든, ImageView든, 자신이 만든 CustomView이든 상관 없다. 위처럼 따로 빼서 커스텀 로직을 작성할 수 있는 것이다.
자, 기존을 생각해보자. 원래는 위와 같이 ui에 데이터를 set해주는 부분을 Activity내에 작성했다. MVC든, MVP든 상관없이 말이다. 하지만, MVVM은 dataBinding + bindingAdapter를 사용함으로써 관심사를 view.xml로 분리할 수 있게 되었다.
그러므로, 내가 이렇게 MVC, MVP패턴들을 관찰해보고 스스로 고찰해보며 든 생각은 다음과 같다.
MVVM패턴을 사용하는 핵심은 dataBinding + bindingAdapter사용하여 관심사를 분리하는데 있다.
그리고 MVVM은 MVP와는 또 다른 장점도 존재한다. 바로, ViewModel을 재사용할 수 있는 것이다. 기존 MVP패턴에서 Presenter의 경우는 불필요한 메소드의 오버라이딩 문제때문에 사용할 수가 없었다. 하지만, ViewModel은 그러지 않아도 된다.
뿐만 아니라, ViewModel은 ShareViewModel을 사용함으로써 호스트 액티비티와 프래그먼트끼리 공유하도록 할 수도 있다. 아...! 이 얼마나 편한가!?
즉, dataBinding + bindingAdapter를 사용하여 관심사를 분리할 수 있는데, 단순히 귀찮다는 이유만으로 액티비티에 데이터를 무분별하게 set하는 행위는 MVVM을 제대로 알고 쓴다고 할 수 없다고 생각한다. (내가... 그러했다...ㅠ) 또한 ViewModel의 재사용성을 고려하지 않고 설계하지 않는다면, 이는 MVP와 별다를게 없어지는 것이다.
자, 그럼 좀 더 깊이 고민해보도록 하자. 지금까지 고찰한 지식을 가지고 어떻게 우리 프로젝트를 더 멋지고 견고하게 만들 수 있을까?
그동안 작성해오며 안드로이드 MVVM을 사용했을때의 장점을 생각해보자면 다음과 같았다.
MVVM의 장점
1. dataBinding + bindingAdapter를 사용하여 ui코드의 관심사를 또 다시 분리할 수 있다.
2. ViewModel을 재사용함으로써 비즈니스로직의 중복을 줄일 수 있다.
현재 내가 진행하고 있는 프로젝트는 직장의 프로젝트라 여기서 공개하기가 좀 그렇다 하지만 내가 생각해봤을 때는 다음의 요소들 정리하 필요할듯 보인다.
ToDoList
- ImageView, TextView Extension함수쪽의 중복을 해결해야 한다
- 특정 페이지에서 ViewModel을 화면단위에 맞게 만들어 놓았다. 예를 들어, AFragment라는 화면이 있는데, AViewModel과 같은 식으로 만들어 놓은 것이다. 이는 ViewModel의 재사용성을 고려한 설계가 아니다.
이 글을 읽는 여러분들도 이에 맞게 자신의 프로젝트에 적용해보면 좋을것 같다. 또는 내용이 이해가 안가는 부분은 반복적으로 읽어보면 좀 더 이해에 큰 도움이 될거라 생각한다.
참고한 자료들
[블로그]
https://academy.realm.io/kr/posts/eric-maxwell-mvc-mvp-and-mvvm-on-android/
[서적]
아키텍처를 알아야 앱 개발이 보인다