Beeeam

Android MVP 패턴 본문

Android

Android MVP 패턴

Beamjun 2023. 3. 15. 23:42

MVP

이전에 포스팅 했던 MVC 패턴에서 Model과 View의 의존성이 높아지는 문제점을 해결하기 위해서 생겨난 개념이다.

Model, View, Presenter로 구성되며, Model과 View의 의존성을 없애기 위해서 둘은 Presenter를 통해서만 동작이 가능하다.

→ Model과 View는 오직 Presenter만 통해서 데이터를 주고 받을 수 있다.

 

MVP 패턴의 흐름을 표현 해봤다. 흐름을 보면 먼저 User요청하면 이를 View가 받는다. 그럼 ViewPresenter에 요청이 왔음을 알린다. 그러면 PresenterModel데이터를 요청하고 데이터를 받아온다. 마지막으로 받아온 데이터를 View에 전달하여 화면에 보여준다.

 

MVC와 차이점

  1. activity / fragment 가 온전히 View 역할만 수행한다.
  2. Controller 대신에 presenter를 사용하는데 presenter는 interface로 구현한다.

이 때 Contract를 사용할 수 있는데 Contract는 interface로 구현하고, 이 안에 View와 Presenter를 각각 interface로 구현한다. 그런데 View와 Presenter는 interface로 구현됐기 때문에 구현체를 생성해야 사용이 가능하다.

그래서 presenter interface를 상속하는 클래스를 생성하고, activity나 fragment에서 view interface를 상속하여 사용한다.

사용이 필수는 아니지만 사용해보니 기능을 한 눈에 볼 수 있는 장점이 있어서 사용하는 것이 좋다는 생각이 든다.

 

장점

  • View와 Model의 의존성이 없다.

단점

  • View와 Presenter가 1:1 관계를 이루어서 View가 많아질 수록 Presenter도 많아진다.
  • 기능이 추가될 때 마다 Presenter가 추가 되어야 한다.

 

MVP 적용 예시

MVC 패턴에서 구현했던 앱을 MVP로 구현하였다.

  • Model에는 데이터 관련 코드인 Data.kt
  • Presenter에는 Contract인 MainContract.kt 와 Presenter인 MainPresenter.kt
  • View에는 MainActivity.kt

이렇게 각 파일들을 관심사에 따라서 분류하였다.

 

Layout 파일

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".View.MainActivity">

    <EditText
        android:id="@+id/edt_id"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="10dp"
        android:hint="아이디"
        app:layout_constraintBottom_toTopOf="@+id/edt_password"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_goneMarginBottom="10dp" />

    <EditText
        android:id="@+id/edt_password"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="10dp"
        android:hint="비밀 번호"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btn_login"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:text="로그인"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/edt_password" />

    <TextView
        android:id="@+id/tv_result"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/btn_login"
        tools:text="성공!" />


</androidx.constraintlayout.widget.ConstraintLayout>

Model (Data.kt)

data class Data(
    var usrName: String? = null,
    var password: String? = null
) {
    fun login (usrName: String?, password: String?): Boolean {
        return (usrName == "abc" && password == "1234")
    }
}

MVC 패턴에서의 model과 차이가 없다.

View (MainActivity.kt)

class MainActivity() : AppCompatActivity(), MainContract.View {

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

        val presenter = MainPresenter(this)

        val edtId = findViewById<EditText>(R.id.edt_id)
        val edtPassword = findViewById<EditText>(R.id.edt_password)
        val btnLogin = findViewById<Button>(R.id.btn_login)
        
        btnLogin.setOnClickListener {
            presenter.login(edtId.text.toString(), edtPassword.text.toString())
        }
    }

    override fun showResult(status: Boolean) {
        val tvResult = findViewById<TextView>(R.id.tv_result)

        if (status) {
            tvResult.text = "성공!"
        } else {
            tvResult.text = "실패.."
        }
    }
}

MVC와 바뀐 점은 Activity가 오로지 View 역할만을 한다는 것이다.

그래서 요청이 들어오면 처리할 데이터를 Presenter에 전달한다.

Presenter (MainContract.kt)

interface MainContract {

    interface View {
        fun showResult(status: Boolean)
    }

    interface Presenter {
        val data: Data
        fun login(id: String?, passwd: String?)
    }
}

Contract에서 View interface, Presenter interface를 선언한다. Contract를 사용하면 기능을 한 눈에 볼 수 있어서 좋은 것 같다.

근데 여기서 선언된 View interface, Presenter interface는 사용하려면 구현체가 필요하다!

(MainPresenter.kt)

class MainPresenter(private val view: MainContract.View): MainContract.Presenter {

    override val data: Data
        get() = Data()

    override fun login(usrId: String?, usrPasswd: String?) {
        var loginStatus = data.login(usrId, usrPasswd)
        view.showResult(loginStatus)
    }
}

Presenter interface를 상속받은 구현체이다. Presenter는 Model과 View 사이에서 중개자 역할을 하기 때문에 Model과 View에 요청을 하는 것을 확인할 수 있다.

 

정리

MVC와 비교해서는 복잡하다는 느낌을 받았다. 근데 Model과 View는 Presenter를 통해서만 데이터를 주고 받을 수 있다는 점을 잘 기억하고 설계를 하니 할 만했던 것 같다. 관심사에 따라서 코드를 분류하니 한 파일에서의 코드의 양이 줄어서 더 깔끔한 느낌을 받았다.

근데 예시 코드는 기능이 1개라서 괜찮았지만 만약 여러 기능이 있는 앱이였으면 Presenter의 양이 엄청 늘었을 것 같다… 생각만 해도 아찔..;