[Android, Kotlin] Architecture Pattern - MVP
Android

[Android, Kotlin] Architecture Pattern - MVP

넘 어려운 아키텍처 패턴

MVC는 초기 앱 만들 때 맨날 쓰던 패턴이라 익숙하다.

하지만 MVP와 MVVM 이 녀석들은 아무리 검색하고 공부해도 헷갈려서 이해가 안 간다.

대체 뭐가 뭔지 1도 모르겠다. 그래서 정리해본다.

 

MVP
Model - View - Presenter

MVP 패턴은 Model, View, Presenter로 구성되어 있는 패턴이다.

도식화 하여 보는 것이 그나마 이해하기 가장 쉬우므로 보자.

 

 

코틀린에서 MVC는 Activity가 V+C를 모두 담당하고, Model과도 연결되어 Activity 혼자 모든 데이터 처리까지 담당하는 구조였다. MVC의 경우, 의존성이 제거된 부분 없이 서로가 서로에게 의존성이 존재하여 유지보수가 힘들며, 테스트 코드를 작성 할 때도 UI 위주의 테스트 코드를 작성해야 하므로 변경이 잦은 UI를 중심으로 한 테스트 코드는 좋지 않다.

MVP 패턴의 경우 위 그림처럼 View와 Presenter , Presenter와 Model간은 서로 상호작용이 이루어지는 구조이나, View와 Model간 의존성을 제거한 구조이다.

즉, View와 Model은 서로를 알지 못하며 Presenter를 통해 상호작용한다.

또한, Presenter의 경우 View와 직접적인 상호작용을 통해 UI 변경 등에 관여하는 것이 아니라, view를 참조하여 view 내의 UI를 변경하는 메소드 등을 호출함으로써 간접적으로 view를 변화시킨다.

다음은 , 구글의 Clean Architecture + MVP에 관련된 그림이다.

 

 

크게 Presentation Layer , Domain Layer , Data Layer로 나누어진 모습이다.

Presentation Layer에서는 View와 Presenter가 상호작용을 한다.

그렇다면 Presenter는 View의 UI를 업데이트 하기 위해 Model에 어떻게 접근하는가요?

위 그림에서 보다시피 Domain Layer가 두둥등장한다.

Clean Architecture에서의 Domain Layer는 Presenter가 Model과 상호작용하기 위한 UseCase 및 Logic으로 구성되어 있다. 쉽게 말해 Model에 접근하여 데이터를 받아오고, 이를 가공하여 Presenter에 전달해주는 역할이다.

보통 각 하나의 동작마다 각각의 UseCase를 구성한다.

ex) 영화 리뷰 앱의 경우 리뷰 작성하는 UseCase , 리뷰 삭제하는 UseCase 등

이는 아래에서 코드를 보며 자세히 알아보자.

Data Layer에서는 Repository를 구성하여 Retrofit과 같은 http , API 통신을 하는 Remote Repository, 로컬 데이터를 받아오는 Local Repository로 나누어 Data를 담당한다.

요약하면,

Presenter는 UseCase를 통해 Repository에서 데이터를 가공해 받아오고, 이를 View에 뿌려준다.

 

 

Example
영화 목록 불러오기

 

요약이 사실 힘들다.

거두절미하고, MVP가 적용된 예제를 보자.

영화 리뷰 앱 중, 홈 화면에서 영화 목록을 보여주는 부분이다.

 

interface MovieReviewsContract {

    interface View: BaseView<Presenter> {

        fun showReviews(reviews: MovieReviews)

        fun showErrorToast(message: String)
    }

    interface Presenter: BasePresenter {
        val movie: Movie

        fun requestAddReview(content: String, score: Float)
    }
}

 

Google에서 내놓은 Clean Architecture의 문법에 따라, Contract interface를 생성 후, 그 안에 View와 Presenter interface를 선언한다.

메소드 이름에서 유추할 수 있듯, View Interface의 메소드는 모두 "show"라는 단어가 들어가 있다.

즉, View에서 사용되는 메소드는 모두 UI와 관련된 메소드이다.

Presenter Interface의 메소드는 모두 "request"라는 단어가 들어가 있다.

즉, Model(data) 부분과 상호작용하여 데이터를 받아오고, view의 메소드를 통해 UI에 그려주는 중간자 역할을 한다.

 

 

class HomePresenter(
    private val view: HomeContract.View,
    private val getAllMoviesUseCase: GetAllMoviesUseCase,
    private val getRandomFeaturedMovieUseCase: GetRandomFeaturedMovieUseCase
) : HomeContract.Presenter {

    override val scope: CoroutineScope = MainScope()

    override fun onViewCreated() {
        fetchMovies()
    }

    private fun fetchMovies() = scope.launch {
        try {
            view.showLoadingIndicator()
            val movies = getAllMoviesUseCase()
            view.showMovies(movies)
        } catch (exception: Exception) {
            exception.printStackTrace()
            view.showErrorDescription("에러가 발생했어요!")
        } finally {
            view.hideLoadingIndicator()
        }
    }
}

 

Home Presenter에서는, fetchMovies에서 UseCase를 통해 추천 영화와 모든 영화 목록을 불러온다.

이후, UseCase를 통해 받아온 추천 영화와 모든 영화 목록을 view의 showMovies 메소드에 넘겨주어 간접적으로 UI에 보여줄 수 있도록 한다. 또한, view의 Indicator를 show, hide하는 UI 관련 메소드 모두 간접적으로 Presenter 측에서 타이밍을 맞추어 호출해주는 것을 볼 수 있다.

 

 

 

override fun showMovies(movies: List<Movie>) {
        (binding?.recyclerView?.adapter as? HomeAdapter)?.run {
            this.movies = movies
            notifyDataSetChanged()
        }
}

 

HomeFragment(View)에서는, showMovies 메소드를 구현한다. Presenter에서 넘겨받은 영화 목록을 실제 UI에 뿌려주는 메소드를 구성하여 UI를 그린다.

 

 

class GetAllMoviesUseCase(
    private val movieRepository: MovieRepository
) {
    suspend operator fun invoke(): List<Movie> =
        movieRepository.getAllMovies()
}

 

GetAllMovieUseCase에서는 movieRepository를 통해 모든 영화 목록을 가져오도록 한다.

 

 

interface MovieRepository {

    suspend fun getAllMovies(): List<Movie>
}

class MovieRepositoryImpl(
    private val movieApi: MovieApi,
    private val dispatchers: CoroutineDispatcher
) : MovieRepository {

    override suspend fun getAllMovies(): List<Movie> = withContext(dispatchers) {
        movieApi.getAllMovies()
    }
}

 

Repository에서는, getAllMovies라는 메소드를 movieApi에서 호출하여 데이터를 Api를 통해 가져온다.

이런식으로 끝에서부터 Repository - UseCase - Presenter - View 순서로 호출된 데이터가 움직이는 걸 볼 수 있다. 확실하게 View와 Model이 분리 된 모습이다.

 

 

MVP가 최고네잉

 

그런데 왜 자꾸 이런 좋은 MVP를 두고 MVVM이 또 나왔을까?

View와 Model이 확실하게 분리된 것은 좋으나, 아직 View와 Presenter간의 의존성은 존재한다.

위 코드에서 보이는 것처럼, 하나의 View에는 하나의 Presenter가 필연적으로 담당해서 존재해야 한다.

이로 인해, View와 Presenter 간 의존성이 더욱 높아지게 된다는 단점이 존재한다.

점점 앱의 사이즈가 커질 경우, 이는 유지보수에 매우 힘들어진다.

앱 사이즈가 작은 경우에는 오히려 MVC가 가장 좋은 아키텍처 패턴이 될 수 있다.

MVVM은 을마나 좋을지, 다음에 알아보자.

 

 

 

 

사진 출처 : https://cometome1004.tistory.com/152

 

코틀린 MVP패턴 아키텍쳐 (1) - 이론

MVP란? - MVC 패턴과는 다르게 View와 Model의 의존성을 제거한 패턴 - Presenter는 View와 Model 사이에 중재자 역할을 한다. - 즉, View와 Model은 서로 알지 못하고 Presenter를 통해 서로 상호작용을 한다. -..

cometome1004.tistory.com

 

https://youngest-programming.tistory.com/111

 

[안드로이드] MVP 디자인패턴 정리 및 예제

[2021-04-13 업데이트] 프로젝트에 MVC 아키텍처만 사용하다가 최근 간단한 공부용 프로젝트를 통해 MVP 아키텍처를 적용해보고있다. 확실히 기존 MVC 구조보다 코드가 정리되는 느낌이 들었다. MVP구

youngest-programming.tistory.com

 

https://github.com/android/architecture-samples/tree/todo-mvp-clean

 

GitHub - android/architecture-samples: A collection of samples to discuss and showcase different architectural tools and pattern

A collection of samples to discuss and showcase different architectural tools and patterns for Android apps. - GitHub - android/architecture-samples: A collection of samples to discuss and showcase...

github.com