MVP 패턴에 대해 알아 보자!

환노·2025년 5월 3일

최근 우테코에서 안드로이드 미션들을 시작하면서 MVP 패턴을 접하게 되었습니다 😵‍💫

저뿐 아니라 크루들 대부분이 해당 개념에 익숙하지 않아, 미션에 자연스럽게 적용하는 데 어려움을 느꼈습니다 🥲

이전 글에서 MVC에 대해 정리했던 것처럼, 오늘은 MVP에 대해 정리해 보겠습니다

MVP 패턴이란?

Model - View - Presenter 의 약자로,
하나의 애플리케이션을 위 세 가지 구성 요소로 구분한 패턴입니다

Model

MVC 패턴에서의 Model과 동일하게 데이터 정보 및 비즈니스 로직을 담당합니다
데이터와 관련된 부분을 담당하며, 값과 기능을 가집니다

즉 비즈니스 로직, DB와의 상호작용, 데이터 가공 역할을 맡아서 할 수 있겠죠 🤔

View

MVC 패턴에서 View와 동일하게 화면에 보여 주는 역할을 합니다

MVC 패턴과 차이점이 있다면 MVP 패턴에서 ViewModel에 대해 전혀 모릅니다

Presenter

MVC 패턴에서 Controller와 유사한 역할을 합니다
View에서 요청한 정보로 Model을 가공하여 View에 전달해 줍니다

앱의 전체적인 플로우는 다음과 같이 흘러갑니다

1. 사용자가 View를 통해 입력(event)를 발생시킨다
2. View는 사용자 입력 (event)에 대해 Presenter에게 어떤 동작을 수행하도록 명령한다
3. Presenter는 해당 명령을 수행하기 위해 Model에게 데이터를 요청하거나, 가공하도록 한다
4. Model은 Presenter의 요청에 응답한다
5. Presenter는 View에게 이벤트 처리 결과를 알린다
6. View는 Presenter가 응답한 데이터를 이용하여 화면을 업데이트 한다

사실 개념으로만 본다면 와닿지 않을 거라고 생각합니다

코드로 예시를 들어 볼까요 😵‍💫

계산기 코드로 알아 보자

📄 xml 코드
<?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:id="@+id/main"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".MainActivity">

  <TextView
      android:id="@+id/text_number"
      style="bold"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="0"
      android:textSize="30sp"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent" />

  <Button
      android:id="@+id/minus_btn"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginTop="20dp"
      android:text="minus"
      app:layout_constraintEnd_toStartOf="@id/plus_btn"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@id/text_number" />

  <Button
      android:id="@+id/plus_btn"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginTop="20dp"
      android:text="plus"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toEndOf="@id/minus_btn"
      app:layout_constraintTop_toBottomOf="@id/text_number" />

</androidx.constraintlayout.widget.ConstraintLayout>

minus 버튼을 누르면 숫자가 감소하고, plus 버튼을 누르면 숫자가 증가하는 간단한 계산기를 만들어 보겠습니다

📄 MVP 패턴을 적용하기 전
class MainActivity : AppCompatActivity() {
  private var number = 0
  private val plusBtn: Button by lazy { findViewById(R.id.plus_btn) }
  private val minusBtn: Button by lazy { findViewById(R.id.minus_btn) }
  private val text: TextView by lazy { findViewById(R.id.text_number) }

  override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)
      enableEdgeToEdge()
      setContentView(R.layout.activity_main)
      ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
          val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
          v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
          insets
      }

      plusBtn.setOnClickListener {
          number++
          text.text = number.toString()
      }

      minusBtn.setOnClickListener {
          number--
          text.text = number.toString()
      }
  }
}

우선 MVP 패턴을 적용하기 전에, PresenterView가 각각 어떤 기능을 제공할지 명시하는 명세서를 작성해야 합니다

Contract

interface MainContract {
    interface MainView {
        fun updateNumber(number: Int)
    }

    interface MainPresenter {
        fun increaseNumber()

        fun decreaseNumber()
    }
}

위 명세서에서 ViewActivity에 해당하며, Number를 업데이트 할 것이라고 알려 주고 있네요

Presenter는 숫자를 증감시킬 예정이라고 하네요 👍🏻

💡 메서드 네이밍에 관한 팁
PresenterActivity는 서로에게 명령하는 관계입니다
따라서 onClick(), onScroll()onXX~() 처럼 명령을 듣는 것처럼 보이는 네이밍은 지양하는 것이 좋다고 합니다 🤯

Model

class Number(var value: Int = 0) {
    operator fun plus(arg: Int) {
        value += arg
    }

    operator fun minus(arg: Int) {
        value -= arg
    }
}

사실 너무 간단한 예시라 Model이 필요없지만, 각 객체가 자신의 일을 할 수 있도록 만들어 봅시다

Presenter

class MainPresenter(
    private val view: MainContract.MainView
) : MainContract.MainPresenter {
    private val number: Number = Number()

    override fun increaseNumber() {
        number + 1
        view.updateNumber(number.value)
    }

    override fun decreaseNumber() {
        number - 1
        view.updateNumber(number.value)
    }
}

PresenterModel에게 값을 변경하도록 요청하고,
이후 변경된 값을 기반으로View에게 화면을 업데이트하라고 명령합니다

여기서 포인트는 PresenterMainActivity가 아닌, ✨MainContract.MainView를 참조✨하고 있다는 것입니다

MVP 패턴의 장점은 ViewModel이 서로 의존하지 않는다는 점입니다
하지만 반대로, ViewPresenter 사이의 결합도는 높아지는 단점도 존재합니다 🥲

이를 해결해 줄 수 있는 친구가 ‼️Contract‼️ 입니다

Contractinterface로 만들어 주지 않으면 ActivityPresenter1:1 종속성을 갖게 되고, 테스트하기도 힘들어집니다
이러한 강한 결합은 구조의 유연성을 떨어뜨리는 원인이 됩니다 🥲

MVP 패턴에서 Contract는 의무가 아니라고 말하지만, 계층 간 역할을 명확히 나누고, 결합도를 낮추기 위해 사실상 필수에 가깝습니다 💪

View

class MainActivity : AppCompatActivity(), MainContract.MainView {
    private val presenter = MainPresenter(this)
    private val text: TextView by lazy { findViewById(R.id.text_number) }
    private val plusBtn: Button by lazy { findViewById(R.id.plus_btn) }
    private val minusBtn: Button by lazy { findViewById(R.id.minus_btn) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContentView(R.layout.activity_main)
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }

        plusBtn.setOnClickListener {
            presenter.increaseNumber()
        }

        minusBtn.setOnClickListener {
            presenter.decreaseNumber()
        }
    }

    override fun updateNumber(number: Int) {
        text.text = number.toString()
    }
}

마지막으로 ViewMainActivity 코드입니다

각 버튼들에 ClickListener를 달아 주는 걸 확인할 수 있는데요, 🤔
각 버튼이 클릭됐을 시 어떤 행동을 해야 하는지 명시해 주고 있습니다

Presenter의 숫자 증감 함수는 view.updateNumber()를 호출하여 화면에 갱신된 값을 노출할 수 있겠죠 🙃


MVP를 적용하기 전의 코드를 보면
ActivityModel의 역할Presenter의 역할을 전부 하고 있었습니다

MVP 패턴을 적용하면 각 객체의 역할이 분리되면서 유지보수가 용이해질 거라고 생각합니다

아직 완전히 적응한 것도 아니고, 부족한 부분도 많지만 반복해서 적용해 나가다 보면 자연스럽게 내 것으로 만들 수 있을 것이라 생각합니다 💪

더 나은 구조나 개선 포인트, 팁들이 있다면 댓글로 알려 주세요 🙏🏻

0개의 댓글