Post

Android에서 MVC, MVP, MVVM 패턴의 특징과 예제

  이번 글에서는 안드로이드 개발에서 가장 흔히 쓰이는 MVC, MVP, MVVM 패턴에 관해 공부한 내용을 공유하겠습니다.

왜 설계 패턴이 필요하지?

  소프트웨어 개발자는 사용자 혹은 고객의 요구 사항에 맞게 프로그램을 제작합니다. 만약 요구사항이 영원히 변하지 않는다면 프로그램도 수정될 필요가 없습니다. 하지만 서비스를 제공하는 업체의 경우, 급격히 변하는 사용자의 요구사항에 맞추어 프로그램도 자주 변경 해야 합니다. 이 상황에 놓인 개발자들은 유지보수하기 좋은 코드를 작성해야 했습니다. 그리고 “유지보수하기 좋은 코드들은 어떤 패턴을 갖는다.” 해서 여러 설계 패턴이 나타나게 됩니다.

예제 코드 소개

https://github.com/wonhaeseong/MVC-MVP-MVVM-study
가판대의 물건을 장바구니에 넣고, 계산하면 장바구니가 모두 비워지는 간단한 앱입니다. MVC, MVP, MVVM 브랜치가 있습니다. 아랫글에 있는 예제 코드는 많은 부분이 생략되어 있습니다. 이해가 안 되면 먼저 코드를 둘러보셔도 됩니다.

MVC

  • Model: 앱의 데이터, 비즈니스 로직, 캐싱, 네트워킹, 데이터베이스 등이 속함
  • View: 휴대폰 화면에 보여지는 부분, 사용자와 상호작용, Controller, Model의 참조를 갖음
  • Controller: Model과 View를 조작 및 연결함

Desktop View

  위의 그림은 전통적인 MVC 구조입니다. 안드로이드에서는 2011년부터 활발하게 사용되었다고 합니다. 사용자는 Controller를 작동시킵니다. 그런데 안드로이드는 분명 휴대폰 화면인 View를 통해 사용자와 상호 작용합니다. 벡엔드의 경우 Client의 Request를 전달하여 Controller가 진입점이 될 수 있지만 안드로이드와 프론트엔드는 다릅니다. 그래서 진입점이 View인 구조를 사용해야 합니다.

Desktop View

  이제 위 그림의 설계를 따르면 될 것 같습니다. Controller와 View는 Activity에 몰아넣고 Model만 다른 클래스로 분리한다고 가정합시다. 만약 Controller를 단위 테스트하려면 어떻게 해야 할까요? 안드로이드 개발 특성상 View와 Activity는 의존성이 큽니다. 안드로이드 프레임워크와 깊게 연관되어 있다면 단위 테스트를 수행하기 어렵습니다. 이 문제를 해결하기 위해 View의 Interface를 만듭니다. Activity가 View Interface를 구현하고 Controller는 View 인터페이스를 참조하면 Controller는 단위 테스트 가능하고 View도 UI 테스트를 수행 할 수 있습니다.

Desktop View

  장바구니에 물건이 추가되는 상황을 예시로 보여 드리겠습니다. 코드의 주석을 따라가시면 이해가 쉽습니다.

1
2
3
interface View {
    fun onBasketItemAdded(index: Int)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class MainActivity : AppCompatActivity(), View {
  
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    model = Repository(Display(),Basket())
    controller = Controller(this, model)
  }

  private fun initDisplayView() {
    displayView.adapter = DisplayAdapter(model.displayItems) { item ->
      controller.onDisplayItemClicked(item) // 1. 사용자 상호작용 알림
    }
  }
  
  override fun onBasketItemAdded(index: Int) { // 4.데이터 가져옴
      basketAdapter.notifyItemInserted(index)
      updateNumberOfBasketItemsView()
  }
  
  private fun updateNumberOfBasketItemsView(){
    numberOfItemsView.text = model.numberOfBasketItems.toString()
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Controller(
    private val view: View,
    private val model: Repository
) {
    fun onDisplayItemClicked(item: Item) {
        val existingItemIndex = model.basketItems.indexOfFirst { item.name == it.name }
        if (existingItemIndex == -1) {
            model.addItemToBasket(item) //2.model 업데이트
            view.onBasketItemAdded(model.basketItems.lastIndex) //3.model 업데이트 알림
        } else {
            model.incrementBasketItemQuantity(existingItemIndex)
            view.onBasketItemUpdated(existingItemIndex)
        }
    }
}

  예제에서 확인할 수 있는 MVC의 장단점을 보겠습니다.
장점: 코드를 이해하기 쉬움
단점: 요구사항이 많아지면 Controller가 매우 커짐, View가 Model의 참조를 갖음(Controller를 거치지 않고 API, DB에 접근 가능)

MVP

  • Model: MVC의 Model과 같음
  • View: 사용자와 상호작용, 사용자의 action을 Presenter에게 전달하거나 데이터를 얻는다.
  • Presenter: Controller와 비슷하지만 View에 Model에서 얻은 데이터를 함께 전달함.

  MVP는 MVC의 View가 더 이상 Model의 참조를 갖지 않도록 개선된 패턴입니다. 이제 Controller를 통해 model도 제공받게 되었습니다. 그리고 Controller는 Presenter로 이름이 변경되는데, ‘무언가를 조작하는 역할’에서 ‘View에 어떤 데이터를 보여줄 것인지 정하는 역할’ 하도록 바뀌었기 때문입니다.

Desktop View

  위 그림에서 View는 더 이상 Model의 참조를 갖지 않습니다. 아래 예제 코드를 보면 쉽게 이해됩니다. 참고로 MVP에서 사용한 View Interface는 MVC의 그것과 약간 달라졌습니다. 그 이유는 Presenter가 ‘View에 어떤 데이터를 보여줄 것인지 정하는 역할’에 집중하려면 Controller보다 View의 기능을 더 세부적으로 조작할 수 있어야 한다고 생각했기 때문입니다. 물론 정해진 것은 없으며, 취향에 따라 추상화 정도를 정하면 됩니다.

1
2
3
4
5
6
interface View {
    fun initView(displayItems: List<Item>, basketItems: List<BasketItem>)
    fun updateBasketOnItemAdded(index: Int)
    fun updateBasketOnItemQuantityIncrement(index: Int)
    fun updateNumberOfBasketItemsView(numberOfBasketItems: String)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Presenter(
    private val view: View,
    private val model: Repository
) {
    init {
        view.initView(
            displayItems = model.displayItems,
            basketItems = model.basketItems
        )
    }

    fun onDisplayItemClicked(item: Item) {
        val existingItemIndex = model.basketItems.indexOfFirst { item.name == it.name }
        if (existingItemIndex == -1) {
            model.addItemToBasket(item) // 2. model 업데이트
            view.updateBasketOnItemAdded(model.basketItems.lastIndex) // 3. 화면 선택
            view.updateNumberOfBasketItemsView(model.numberOfBasketItems.toString()) // 3. 화면 선택
        } else {
            model.incrementBasketItemQuantity(existingItemIndex)
            view.updateBasketOnItemQuantityIncrement(existingItemIndex)
            view.updateNumberOfBasketItemsView(model.numberOfBasketItems.toString())
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class MainActivity : AppCompatActivity(), View {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        presenter = Presenter(this, Repository(Display(), Basket()))
    }

    override fun initView(displayItems: List<Item>, basketItems: List<BasketItem>) {
        initBasketView(basketItems)
        initBasketBtn()
        initDisplayView(displayItems)
    }

    private fun initDisplayView(items: List<Item>) {
      val displayView = findViewById<RecyclerView>(R.id.display)
      displayView.adapter = DisplayAdapter(items) { item ->
        presenter.onDisplayItemClicked(item) // 1. 사용자 상호작용 알림
      }
    }
  
    override fun updateBasketOnItemAdded(index: Int) {
      basketAdapter.notifyItemInserted(index) 
    }
  
    override fun updateBasketOnItemQuantityIncrement(index: Int) {
      basketAdapter.notifyItemChanged(index)
    }

    override fun updateNumberOfBasketItemsView(numberOfBasketItems: String) {
      numberOfItemsView.text = numberOfBasketItems
    }
}

  MVP는 장점이 매우 많은 것 같습니다. Model, View가 서로 강하게 연관되어 있지 않습니다. Model이 크게 변하더라도 View는 신경 쓸 필요가 없습니다.
단점을 찾아보면 Presenter가 GOD Object이며 크기가 매우 커질 수 있다는 것입니다. 크기가 커지는 경우 클래스를 잘 세분화한다면 큰 문제가 되지는 않을 것 같습니다.
즉, 우리는 MVP로 충분히 좋은 설계를 할 수 있습니다.

MVVM

  • Model: MVC의 Model과 같음
  • View: 사용자와 상호작용, 사용자의 action을 ViewModel에게 전달하고 데이터 Stream을 observing함.
  • ViewModel: Presenter와 비슷한 일을 하지만 View에 대해서 모름. observable한 데이터를 가지고 외부에서 그것을 observing 할 수 있게함.

  MVP에서 Presenter가 View를 직접 업데이트 하던 것을 View가 ViewModel의 데이터를 observing하여 변경이 있을 때 View를 업데이트하도록 했습니다. observing은 흔히 Livedata와 Android Architecture Component 중 하나인 ViewModel 클래스(이하 AAC ViewModel)로 설계됩니다. Livedata는 Activity, Fragment 등의 생명주기를 인지하는 observable한 data holder입니다. 참고로 AAC ViewModel은 MVVM과 관련이 없습니다. Jetpack의 ViewModel은 Configuration Change(예: 화면 회전)에서도 데이터를 캐싱하여 값을 유지하기 위해 만들어졌습니다. 그래서 MVVM 구조에서 ViewModel이 반드시 Jetpack의 ViewModel이어야만 하지는 않습니다. 어떤 것이든 ViewModel의 역할을 수행할 수 있다면 됩니다.

Desktop View

  MVVM의 예제 코드를 간략히 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class MainViewModel(
    private val model: Repository
) : ViewModel() {
    val displayItems: List<Item> = model.displayItems

    private val _basketLiveData: MutableLiveData<List<BasketItem>> by lazy {
        MutableLiveData<List<BasketItem>>(model.basketItems)
    }
    val basketLiveData: LiveData<List<BasketItem>>
        get() = _basketLiveData

    private val _numberOfItemsLiveData: MutableLiveData<Int> by lazy {
        MutableLiveData<Int>(model.numberOfBasketItems)
    }
    val numberOfItemsLiveData: LiveData<Int>
        get() = _numberOfItemsLiveData

    fun addItemToBasket(item: Item) {
        val existingItemIndex = model.basketItems.indexOfFirst { item.name == it.name }
        if (existingItemIndex == -1) {
            model.addItemToBasket(item) // 3. model 업데이트
        } else {
            model.incrementBasketItemQuantity(existingItemIndex)
        }
        _basketLiveData.value = model.basketItems
        _numberOfItemsLiveData.value = model.numberOfBasketItems
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class MainActivity : AppCompatActivity() {
  
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
      
        val basketObserver = Observer<List<BasketItem>> {
            basketAdapter.setItem(it)
        }
        val numberOfBasketItemsObserver = Observer<Int> {
            numberOfItemsView.text = it.toString()
        }
        
        viewModel.basketLiveData.observe(this, basketObserver)     // 1. 데이터 observing
        viewModel.numberOfItemsLiveData.observe(this, numberOfBasketItemsObserver)
    }

    private fun initDisplayView(items: List<Item>) {
        val displayView = findViewById<RecyclerView>(R.id.display)
        displayView.adapter = DisplayAdapter(items) { item ->
            viewModel.addItemToBasket(item)  //2. 사용자 상호작용 알림
        }
    }
}

  ViewModel은 observable한 data를 내놓고 View가 그것을 observing하여 View를 갱신합니다.
MVVM는 현재 많은 프로젝트에서 사용되고 있습니다. MVVM이 가진 유용함만큼이나 앱 생명주기와 같이 중요하게 신경 써야 할 부분이 생깁니다.

결론

MVC, MVP, MVVM 중 무조건 좋은 것은 없습니다. 프로젝트 규모가 크고 Data Stream이 복잡하여 Presenter가 복잡해질 것 같다면 MVVM을 선택할 수 있고, 팀원이 반응형 프로그래밍에 경험이 없거나 소규모 프로젝트라면 MVP가 선택지가 될 수 있습니다.

참조

Android Test-Driven Development by Tutorials, reywenderlich Tutorial Team, p91 ~ p98

제 글에 잘못된 부분 또는 다른 의견이 있으시면 댓글을 통해 남겨주세요. 토론은 언제나 환영입니다.

This post is licensed under CC BY 4.0 by the author.