Android/Theory

MVC, MVP, MVVM 봐도 봐도 조금씩 헷갈리면 모르는 거임

sh1mj1 2023. 10. 2. 13:20

안드로이드에서 유명한 패턴인데 봐도 봐도 조금씩 헷갈리는 것 같아서 제대로 정리를 할 필요성을 느껴 정리합니다.

 

MVC(Model - View - Controller) 패턴 

MVC 패턴은 다른 소프트웨어 개발에서 많이 사용되는 디자인 패턴입니다.

당연히 안드로이드 앱 개발에서도 많이 사용됩니다.

 

MVC 패턴은 앱을 세 가지의 주요 컴포넌트로 나누어서 관리하며, 각 컴포넌트는 다른 역할을 수행합니다.

왼쪽: https://www.freecodecamp.org/news/the-model-view-controller-pattern-mvc-architecture-and-frameworks-explained/   오른쪽: https://brunch.co.kr/@mystoryg/170

Model - MVC

  • Model 은 데이터와 데이터 관련 로직을 포함하는 컴포넌트.  데이터는 앱의 핵심 데이터나 상태.
  • 안드로이드 앱에서의 Model 은 데이터베이스나 네트워크 요청 결과, 앱의 상태 등을 포함할 수 있음.
  • Model 은 주로 데이터를 가져오고 저장하며, 데이터의 유효성 검증을 하고 가공할 수 있음.

View - MVC

  • View 는 UI(User Interface)를 나타내고 화면에 데이터를 표시하고, 사용자 입력을 처리함.
  • 안드로이드 앱에서의 뷰는 xml 레이아웃 파일로 정의되거나 코드로 직접 생성(Jetpack Compose)될 수 있음.
  • View 는 주로 사용자에게 정보, Model 의 상태를 표시하고, 사용자 입력을 받아서 Controller 에 전달함.
  • xml 컴포넌트에 입력이 있을 때 onClick 속성을 통해서 어떤 함수가 호출되어야 하는지 정도를 설정할 수 있음.
    물론 실제 동작 처리 구현은 컨트롤러의 함수에서 이루어진다..

Controller - MVC

  • Controller 는 Model 과 View 의 중간 역할, 가교 역할을 수행함.
    사용자 입력을 처리하고 Model 에서 데이터를 가져와서 View 에 전달함.
  • 안드로이드 앱에서의 컨트롤러는 주로 Activity 나 Fragment 임.
  • Controller 는 사용자 상호작용을 감지, 해당 이벤트를 처리하여 Model 과 View 를 업데이트함.
  • View 가 사용자 입력을 받으면 Controller 에게 알리고, Controller 는 적절한 조치를 취하기 위한 동작을 함.
      예를 들면 사용자로부터의 입력을 기반으로 모델과 상호작용하고 UI 상태를 갱신할 수 있다.
  • View 와 실제로 연결되고 onClicklistener 등을 통해 입력을 입력받았을 때 처리할 동작을 구현할 수 있음.
  • 비즈니스 로직, UI 로직이 Controller 에서 처리됨.

 

안드로이드 앱을 개발해보았으면 한 번쯤은 MVC 패턴으로 앱을 만들어 보았을 것입니다. 그만큼 기초적이며 쉽다는 것이죠.

하지만 Model 과 View 사이에 완전히 제거할 수 없는 의존성이 발생하며, 스파게티 코드가 될 가능성이 많습니다.

Controller 의 부담이 너무 커져서 MVC 패턴을 흔히 Massive ViewController 라고 하기도 하죠.

 

그러면 이 MVC 를 테스트 관점에서 봅시다.

 

테스트 관점에서 MVC

View 와 Model 이 분리되어서 Model 에 대한 테스트를 작성하기 쉬워집니다. (의존성이 완전히 제거된 것이 아님)

하지만 View 에서 Controller 의 이벤트를 통지받고, Model 도 함께 사용하기 때문에, 즉, Controller 에서 UI 로직과 비즈니스 로직을 모두 처리하기 때문에 View 와 Controller 의 테스트는 복잡해집니다.

또 Controller 가 안드로이드 API 에 깊게 종속되므로 Controller 의 유닛 테스트도 어려워지죠.

 

최소한 비즈니스 로직이라도 분리되면 테스트 가능성이 열리고, Controller 에서의 View가 아닌 로직에 대한 테스트가 가능해진다.

 

MVP(Model - View - Presenter) 패턴

위 MVC 패턴의 단점 때문에 나온 것이 MVP 패턴입니다.

MVP 아키텍처의 경우 Controller 를 View 에서 정의하던 기존의 MVC 패턴과 다르게 Presenter 가 그것을 대신해 줍니다.

MVP 패턴에서는 MVC 의 Controller 역할을 View 에서 처리하게 됩니다.

 

Model - MVP

  • MVC 에서의 Model 과 동일한 역할을 함.
  • View 또는 Presenter 등 다른 어떤 요소에도 의존적이지 않은 독립적인 영역임.

View - MVP

  • UI 를 담당하며 xml 과 Activity, Fragment 가 View 임. (MVC 에서의 Activity, Fragment 가 View 로 넘어옴)
  • Model 에서 처리된 데이터를 Presenter 를 통해 받아서 유저에게 보여줌.
  • 사용자의 Action 및 액티비티/프래그먼트 상태 변경을 주시하며 Presenter 에게 보내는 역할을 함.
  • View 는 사용자 입력 이벤트를 Presenter 에 전달하고 Presenter 에게 데이터를 표시하도록 요청함. (Presenter 에 매우 의존적임)

Presenter - MVP

  • Model 과 View 사이의 매개체 역할을 함.
  • Controller 처럼 View 에 직접 연결되어 상호작용하는 것이 아닌,  인터페이스와 연결됨.
  • 뷰에게 표시할 내용(데이터)만 전달함. 어떻게 보여줄 지는 View 가 담당함. (View 가 인터페이스를 구현해서)
  • Presenter 에서 비즈니스 로직이 처리됨.

View 와 Presenter 가 인터페이스를 통해 상호작용합니다. 

 

아마 코드는 거의 보신 적이 없을 텐데 아래처럼 사용되고는 합니다.

interface MyView {
    fun showData(data: String)
    fun showError(error: String)
}
// --
interface MyPresenter {
    fun fetchData()
    fun detachView() // View와의 연결 해제
}
// --

class MyPresenterImpl(private var view: MyView?) : MyPresenter {
    
    override fun fetchData() {
        // 비즈니스 로직을 수행하고 데이터를 가져옵니다.
        val data = fetchDataFromModel()

        // View에 데이터 표시
        if (data != null) {
            view?.showData(data)
        } else {
            view?.showError("Error fetching data")
        }
    }

    override fun detachView() {
        view = null // View와의 연결 해제
    }

    // 모델에서 데이터 가져오는 메서드 (가상의 메서드)
    private fun fetchDataFromModel(): String? {
        // 모델에서 데이터 가져오는 로직을 구현합니다.
        return "Sample data"
    }
}

class MyActivity : AppCompatActivity(), MyView {
    private var presenter: MyPresenter? = null

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

        // Presenter 초기화
        presenter = MyPresenterImpl(this)

        // 데이터 가져오기 요청
        presenter?.fetchData()
    }

    override fun showData(data: String) {
        // 데이터를 화면에 표시하는 코드
        val textView = findViewById<TextView>(R.id.textView)
        textView.text = data
    }

    override fun showError(error: String) {
        // 에러 메시지를 화면에 표시하는 코드
        Toast.makeText(this, error, Toast.LENGTH_SHORT).show()
    }

    override fun onDestroy() {
        super.onDestroy()
        // 액티비티가 종료될 때 Presenter와의 연결 해제
        presenter?.detachView()
    }
}

MVP 패턴은 몇가지 구현 상세 사항에 대한 가이드라인이 없어 유연성이 높아 프로젝트에 따라 구현 방식이 다를 수 있습니다.

 

테스트 관점에서의 MVP

Presenter 는 기본적으로 뷰에 연결되는 것이 아닌 인터페이스가 연결되는 것이기 때문에 테스트도 용이해지고, 모듈화와 유연성 문제가 해결됩니다.

View 와 Presenter 가 1 대 1 대응으로 View 인터페이스를 구현했다면 Presenter 로직을 쉽게 테스트할 수 있게 되죠. MVP 패턴에서는  Presenter 는 어떤 안드로이드 API 나 코드를 참조하지 않아야 한다는 제약사항이 있어서 더 테스트하기 쉽습니다. (혹은 최대한 사용 안하도록)

 

하지만 시간이 지나면서 Presenter 에도 추가 비즈니스 로직이 점점 모이는 경향이 있습니다. 크고, 변경에 유연하지 않고, 분리하기도 어려운 Presenter 가 되는 것이죠..

또 MVP 패턴은 View 와 Model 간의 상태 변화를 관리하기도 어렵습니다. View 에서 발생하는 이벤트에 따라 Model 의 상태를 변경하면, 이를 다시 View 에 반영하려면 Presenter 에서 많은 작업을 해주어야 합니다.

 

MVVM(Model - View - ViewModel) 패턴

MVVM 패턴에서는 View 의 상태를 알 수 있는 ViewModel 이 등장합니다. 이로써 View 와 ViewModel 간의 완전한 분리가 가능해지죠. MVVM 아키텍처 패턴으로 앱의 코드 관리, 유지 보수가 쉬워집니다.

MVVM 아키텍처에서 ViewModel 에서는 UI 에 대한 작업을 하지 않습니다.

ViewModel 에서는 오직 데이터의 업데이트만 실시하고, View 에서는 해당 데이터를 보여주기만 하면 됩니다.

각 컴포넌트를 다시 자세히 봅시다.

 

Model - MVVM

  • Model 은 데이터와 데이터 관련 로직을 관리하는 부분.
  • DB, 네트워크 요청, 파일 시스템과 같은 데이터 소스와 상호 작용하여 데이터를 가져오고 저장함.
  • Model 은 주로 데이터의 유효성 검사, 데이터 가공 및 비즈니스 논리를 담당함.

View - MVVM

  • View 는 UI(User Interface) 를 표시하고 사용자 입력을 처리하는 부분임.
  • 안드로이드에서 Activity, Fragment, XML 레이아웃 등이 View 역할.
  • View 는 주로 UI 를 표시하고 사용자 입력(UI Event)을 ViewModel 에 전달함.
  • UI 상태를 관리하고 ViewModel 로부터 데이터를 표시하는 역할을 함

ViewModel - MVVM

  • ViewModel 은 View 와 Model 사이의 중간 역할을 수행함.
  • UI 와 관련된 로직을 분리하고 View 를 위한 데이터를 준비함.
  • ViewModel 은 관찰 가능한(Observable) 데이터를 제공함. (Model 을 래핑해서)
    View 에서 필요한 데이터를 관찰하여 데이터가 변경될 때 자동으로 업데이트됨.
    LiveData 나 RxJava 와 같은 도구를 사용하여 데이터 바인딩을 구현할 수 있음.
  • ViewModel 은 주로 생명 주기 관리와 UI 상태 관리에 사용됨.
  • Mediator 라고도 하고 Data holder 라고도 함.
  • 비즈니스 로직은 주로 ViewModel 에서 처리됨.

 

이렇게 ViewModel 이 상태를 관찰함으로써 Reactive 프로그래밍이 가능해집니다. 

또 ViewModel 에서 viewBinding, dataBinding 으로 작성 코드가 많이 줄어들고, 쉽게 UI 를 변경할 수 있게 됩니다.

 

테스트 관점에서의 MVVM

MVVM 아키텍처의 핵심 아이디어는 UI 와 비즈니스 로직을 철저히 분리해서 각 컴포넌트의 역할을 명확하게 정의하고 의존성을 관리하는 것 입니다. 

View 와 ViewModel 간의 독립이라는 것은 UI 와 다른 로직이 독립적으로 분리된다는 것입니다.

즉, 

이로써 코드의 가독성과 유지 보수성이 향상되며, UI와 비즈니스 로직을 독립적으로 테스트할 수 있게 됩니다. Context 와 분리되어 Mockito 를 가지고서도 테스트가 가능해지는 것입니다.

 

안드로이드 앱에서는 MVVM 아키텍처를 구현하기 위해서 Android Jetpack 라이브러리의 ViewModel 및 LiveData 와 같은 라이브러리를 활용할 수 있습니다. MVVM 아키텍처는 안드로이드 앱의 구조를 더 모듈화하고 유연하게 만들어주므로, 대규모 앱 또는 복잡한 UI 를 다루는 데 유용합니다.

 

MVVM 패턴을 사용하려면..

MVVM 패턴을 무조건적으로 사용해야 하는 것은 아닙니다.

사실 MVVM 패턴은 위 다른 패턴보다 비교적 복잡한 아키텍처 패턴으로 작은 규모 앱에서는 불필요하게 복잡할 수도 있습니다.

그로 인해 MVVM 패턴에 대한 정확한 이해가 덜 잡혀있다면 오히려 변경, 유지 보수를 하기 어렵고, 메모리나 CPU 오버헤드가 발생하는 잘못된 설계를 할 수도 있습니다. 러닝 커브가 높은 것이 문제가 되는 것이죠.

 

하지만 대부분 안드로이드 네이티브 앱을 개발하는 회사에서는 MVVM 패턴을 사용합니다.

그리고 아래와 같은 기술들을 사용하겠죠.

 

  • MVVM 패턴에 AAC-ViewModel
  • Dagger 혹은 Hilt, Koin 같은 DI 라이브러리
  • RxJava, Coroutines, Coroutines - Flow
  • LiveData, Coroutines StateFlow, SharedFlow ...
  • DataBinding
  • .....

그렇다면 옵저버 패턴, 비동기 프로그래밍, DI(의존성 주입), 등 등의 것들을 이해해야 하죠....

RxJava 를 제외하고 모두 기존 프로젝트에서 활용한 기술이지만, 분명 어느 부분에서 잘못 설계한 부분이 있을 것 같네요..

 

이것도 천천히 정리를 해보아야 겠습니다!..

 

 

참고 링크

https://android-developer.tistory.com/entry/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-MVVM-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80-%ED%95%84%EC%9A%94%EC%84%B1%EA%B3%BC-%EA%B7%B8-%EB%B0%B0%EA%B2%BD-1

https://medium.com/nerd-for-tech/android-architecture-mvvm-mvp-mvc-and-why-you-should-care-65aae61fc11c

https://brunch.co.kr/@mystoryg/170

https://velog.io/@jojo_devstory/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90-%ED%8C%A8%ED%84%B4-MVC%EA%B0%80-%EB%AD%98%EA%B9%8C

https://thdev.tech/android/2022/12/12/Android-Follow-MVVM-Intro/