[Android] MVP 패턴

핑구·2023년 5월 8일
0

Android

목록 보기
2/8
post-thumbnail

서론

이번에 새로 배우게 된 패턴.
너무 어렵지만 미션을 할 시간이 없어서 제대로 공부하지 못한 채 감에만 의존한 상태로 미션을 진행했다.
이제 미션이 끝나고 프롤로그도 작성해야하니 mvp를 정리해보자

디자인 패턴

먼저 이런 MVC, MVP는 뭘까.
우리는 이를 디자인 패턴(=아키텍처 패턴)이라고 한다.
디자인 패턴을 적용하는 이유는?
개발자의 업무의 대다수는 본래 있는 코드를 유지 보수하는 것이다.
그러기 위해서는 많은 사람들이 읽기 편하고 고치기 편한 코드를 작성할 줄 알아야 한다.
이때 필요한 것이 아키텍처이다.

소프트웨어 아키텍처란 소프트웨어의 구성요소들 사이에서 유기적 관계를 표현하고 소프트웨어의 설계와 업그레이드를 통제하는 지침과 원칙이다. (위키피디아)

아키텍처란 간단하게 설계할 때 각 구성 요소들을 배치하는 구조를 말한다.
아키텍처의 기본은 분리독립이다. 기능을 추가하거나 수정 혹은 제거한다고 해서 다른 요소들에 영향을 주지 않게끔 하기 위함이다.

구글에 검색해보면 아주 다양한 아키텍처가 존재한다.
제이슨의 말에 따르면 각 개발자들이 문제를 해결하다보니 비슷한 구조가 반복되어 이를 패턴화 시켰다고 한다.

소프트웨어 디자인 패턴(software design pattern)은 소프트웨어 공학의 소프트웨어 디자인에서 특정 문맥에서 공통적으로 발생하는 문제에 대해 재사용 가능한 해결책이다. (위키피디아)

각 아키텍처 패턴에 대한 학습을 한 후 억지로 이걸 적용시켜 코드를 작성하려고 하기 보다는
이런게 있군~ 정도로 넘어간 후에 나중에 필요한 상황이 왔을 때 사용해보는 것이 효과적이라고 말씀해주셨다.

다시 돌아와서 가장 기초적인 디자인 패턴이 MVC(Model View Controller) 패턴이다.

MVC (Model View Controller)

아주 간단하게 설명하면
Modle: 데이터
View: 화면
Condtroller: View와 Model을 이어주는 역할

안드로이드를 배우기 전에는 MVC패턴을 사용하여 view와 controller를 명확하게 분리하는 것이 가능했다.
하지만 안드로이드를 배우기 시작하자 view와 controller의 경계가 무너지기 시작했다.

view(xml)에서의 입력에 대한 이벤트 처리가 다시 view에 갱신해줘야 했기 때문에 사실상 controller(activity, fragment)까지 view의 역할을 하고 있었다.

모든 것을 Activity나 Fragment에서 처리한다는 점에서 구조가 단순하여 개발할 때는 빠르게 할 수 있다는 장점이 있다.
하지만 이러한 구조는 Activity나 Fragment가 가지는 역할이 너무 커져 유지 보수가 어렵다.
또한 controller가 안드로이드 의존적이어서 테스트가 힘들다는 단점이 있다.

MVP (Model View Presenter)

MVC 패턴의 단점을 보완하여 만들어진 것이 MVP 패턴이다.
Model: 데이터
View: 화면(activity, fragment 포함)
Presenter: Model과 View 연결 다리로서 Model에서 정보를 받아 UI 갱신 요청하는 역할

view는 입력 이벤트를 전달하고 갱신하는 역할만 하고 최대한 내부적인 처리는 알지 못해야 한다.
모든 비지니스 로직은 model에서 이루어지고 presenter는 이를 view에게 전달해주는 느낌이다. presenter에서는 최대한 안드로이드 의존성이 없어야 한다.
이러한 mvp구조는 인터페이스로 구현하게 된다.
그렇기 때문에 presenter의 테스트가 쉽다.
하지만 mvc와 마찬가지로 presenter에 역할이 집중된다면 성능이 저하되고 유지보수가 어려워진다.


각 패턴마다 장단점이 있기 때문에 뭐가 좋고 나쁘고가 없이 주어진 상황에 맞는 패턴을 사용하면 될 것 같다.

MVP 예시

그렇다면 MVP를 어떻게 구현하는지 실습했던 코드를 통해 알아보겠다.

버튼에 따라 가운데 숫자가 달라지는 기능을 구현한다.

예시 코드

Contract

interface Contract {

    interface View {
        var presenter: Presenter
        fun setCounterText(count: Int) // UI 갱신
    }

    interface Presenter {
        fun add()
        fun sub()
    }
}

Contract 인터페이스는 Presenter와 View 사이에 어떤 기능이 있는지 한눈에 파악할 수 있도록 명시하는 역할로서 필수적이지는 않다.

Activity

class CounterActivity : AppCompatActivity(), Contract.View {

    override lateinit var presenter: Contract.Presenter

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

        presenter = CounterPresenter(this)

        val plusBtn = findViewById<Button>(R.id.btn_plus)
        plusBtn.setOnClickListener {
            presenter.add()
        }

        val minusBtn = findViewById<Button>(R.id.btn_minus)
        minusBtn.setOnClickListener {
            presenter.sub()
        }

    }

    override fun setCounterText(count: Int) {
        val textView = findViewById<TextView>(R.id.tv_count)
        textView.text = count.toString()
    }
}

Contract의 View를 구현한다.
각 입력에 대해서 presenter의 함수를 호출한다.
setCounterText()는 UI 갱신 함수로서 presenter측에서 호출된다.

Presenter

class CounterPresenter(val view: Contract.View) : Contract.Presenter {

    private val counter = Counter()

    override fun add() {
        view.setCounterText(counter.add())
    }

    override fun sub() {
        view.setCounterText(counter.sub())
    }
}

Contract의 Presenter를 구현한다.
Counter라는 모델 객체를 가지며 이를 이용해 UI 갱신 함수를 호출한다.

Model

class Counter {
    private var count = 0

    fun add(): Int {
        return ++count
    }

    fun sub(): Int {
        return --count
    }
}

동작 과정

사용자가 +버튼을 누른다면 view에서 presenter의 add()가 호출된다.
Presenter는 갖고 있는 model의 로직을 사용하여 변경된 model의 값을 받은 후 이를 view에 전달한다.
view는 presenter에서 전달받은 값으로 다시 UI를 갱신한다.

테스트

MVP의 장점인 테스트를 살펴보겠다.
presenter에서 UI 업데이트를 위해 view에 전달하는 값을 테스트한다.

class PresenterTest {

    private lateinit var presenter: CounterPresenter
    private lateinit var view: Contract.View

    @Before
    fun setUp() {
        view = mockk()
        presenter = CounterPresenter(view)
    }

    @Test
    fun 플러스버튼을_누르면_숫자가_올라간다() {
        // given
        val slot = slot<Int>()
        every { view.setCounterText(capture(slot)) } returns Unit

        // when: 누르면(request Data)
        presenter.add()

        // then: 숫자 올라간다
        val actual = slot.captured
        assertEquals(1, actual)
        verify { view.setCounterText(actual) }
    }

    @Test
    fun 마이너스버튼을_누르면_숫자가_내려간다() {
        // given
        val slot = slot<Int>()
        every { view.setCounterText(capture(slot)) } returns Unit

        // when: 누르면(request Data)
        presenter.sub()

        // then: 숫자 내려간다
        val actual = slot.captured
        assertEquals(-1, actual)
        verify { view.setCounterText(actual) }
    }
}

mockk를 사용해서 가짜 view 객체를 생성한다.
verify는 단지 호출이 되는지를 확인하지만 그 값이 옳은지를 보기 위해서 capture를 활용하여 비교했다.


참고
https://velog.io/@its-mingyu/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4MVC-MVP-MVVM
https://velog.io/@bang/Android-MVP-pattern-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0

profile
발전중

0개의 댓글