안드로이드/정리(Android)

Android) MVC, MVP, MVVM 아키텍처 비교 및 구현

김염인 2022. 10. 29. 15:17

MVC, MVP, MVVM 구현

 

아키텍처를 이용하지 않는다면 혼돈의 카오스 구조이다. Layout에 로직판정, 데이터 처리, 액티비티 안에서 모든 것을 구현한다. 하지만 아키텍처 구조를 이용한다면 비즈니스 로직과 UI를 구분하고 정의할 수 있으므로 팀단위 프로젝트에 효율적인 구조라고 말하고 싶다.

 

 

 

 

 MVC 패턴

 

MVC 패턴은 Model + View + Controller로 이루어져 있습니다.

 

<동작 과정>

1. User의 Input 값이 Controller를 통해서 들어오고 Model을 변경해 줍니다.

2. Controller는 View와 1:N으로 이루어져 있는 상태 입니다. 따라서 Controller는 변경된 Model에 적용된 View를 선택 하게 해줍니다.

3. View는 Model을 이용하여 화면에 나타나게 됩니다.

 

단점 : Controller의 책임감이 큰 아키텍처 구조이다. 따라서 규모가 커질 수록 Controller의 무게도 커지게 된다는 단점이 있다. 또한 View와 model의 의존성이 크므로 좋지 않은 구조라고 말하고 싶다.

 

 

< 정리 >

1. MVC 패턴에서는 Model과 View과 완전히 분리되므로 Model은 쉽게 Test 가능
2. Controller가 Activity에 완전히 종속 되기 때문에 테스트가 어려워짐
3. 안드로이드 특성상 액티비티가 View표시와 Controller 역할을 같이 수행해야 하기 때문에 두 요소의 결합도가 높아짐
4. 많은 코드가 Controller로 모이게 되어 액티비티가 비대해짐

 

 

 

 MVP 패턴

 

MVP 패턴은 Model + View + Presenter로 이루어져 있습니다.

 

Model과 View는 MVC 패턴과 동일하고, Controller 대신 Presenter가 존재합니다. 

 

MVP 패턴의 동작 순서는 아래와 같습니다.

  1. 사용자의 Action들은 View를 통해 들어오게 됩니다.
  2. View는 데이터를 Presenter에 요청합니다.
  3. Presenter는 Model에게 데이터를 요청합니다.
  4. Model은 Presenter에서 요청받은 데이터를 응답합니다.
  5. Presenter는 View에게 데이터를 응답합니다.
  6. View는 Presenter가 응답한 데이터를 이용하여 화면을 나타냅니다.

 

 

< 정리 >

1. MVP 패턴은 View와 Model 사이의 데이터 흐름이 사라지고 Presenter가 중간에서 데이터 흐름을 제어
2. 인터페이스를 추가로 구현해야 하기 때문에 구현비용이 올라감
3. View와 Presenter가 1:1로 대응해야 하기 때문에, 앱이 커질 수록 두 요소의 의존성이 강해짐,

 

 

 

 

 MVVM 패턴

 

MVVM 패턴은 Model + View + View Model로 이루어져 있습니다.

 

MVVM 패턴의 동작 순서는 아래와 같습니다.

  1. 사용자의 Action들은 View를 통해 들어오게 됩니다.
  2. View에 Action이 들어오면, Command 패턴으로 View Model에 Action을 전달합니다.
  3. View Model은 Model에게 데이터를 요청합니다.
  4. Model은 View Model에게 요청받은 데이터를 응답합니다.
  5. View Model은 응답 받은 데이터를 가공하여 저장합니다.
  6. View는 View Model과 Data Binding하여 화면을 나타낸다.

 

< 정리 >

1. MVVM 패턴은 View와 Model 사이에 의존성이 없으며 ViewModel도 View에 의존성을 가지지 않음 
2. 참조는 View > ViewModel > Model 순으로 단방향으로 일어난다.

 

 

 

 

 

 

 

MVC, MVP, MVVM 패턴 직접 구현

MVC, MVP, MVVM을 활용 해보기 위한 간단한 앱을 구현하였다.

 

 

 

 

 

 

 

 

 

 

MVC 패턴 구현

간단하게 퀴즈 형식의 toast 메시지를 출력하는 앱을 만들었다.

 

 


Model 영역
package com.example.mvcmvpmvvmpattern.mvc.model

data class Answer(
    var result : String? = null
){
    fun get_result(result: String?) : Boolean{
        if(result == ans){
            return true
        }
        else{
            return false
        }
    }

    companion object{
        const val ans = "서울"
    }
}

Model 영역은 DATA와 비즈니스 로직을 담당한다, 즉 UI와 관련이 없는 앱으로 따지자면 두뇌의 역할을 하는 구조 구현을 할 수 있다.

여기서는 Result로 들어온 값이 "서울"이라는 값과 동일한지 판단하는 역할을 하고 동일 하다면 True를 Return하는 간단한 구조로 구현을 하였다.

 


VIEW 영역
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <TextView
        android:id="@+id/quetion"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintVertical_chainStyle="packed"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toTopOf="@id/edit_text_view"
        android:text="대한민국의 수도는 ?"
        android:textSize="30dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <EditText
        android:id="@+id/edit_text_view"
        app:layout_constraintTop_toBottomOf="@+id/quetion"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:layout_marginTop="20dp"
        android:hint="입력하기"
        android:textAlignment="center"
        android:layout_width="100dp"
        android:layout_height="wrap_content"/>

    <Button
        android:id="@+id/answerButton"
        app:layout_constraintTop_toBottomOf="@id/edit_text_view"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:text="제출하기"
        android:layout_marginTop="20dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</androidx.constraintlayout.widget.ConstraintLayout>

 - 사용자에게 보여지는 UI화면이다. 대표적으로 Android 에서는 레이아웃을 활용한다.

 


Controller 영역
package com.example.mvcmvpmvvmpattern.mvc

import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.example.mvcmvpmvvmpattern.databinding.ActivityMvcBinding
import com.example.mvcmvpmvvmpattern.mvc.model.Answer

class MvcActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMvcBinding

    private lateinit var answer : Answer

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMvcBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)

        answer = Answer()

        binding.answerButton.setOnClickListener {
            val answerResult = binding.editTextView.text.toString()

            if(answer.get_result(answerResult)){
                Toast.makeText(this, "${answerResult}는 정답 입니다.", Toast.LENGTH_SHORT).show()
            }
            else{
                Toast.makeText(this, "${answerResult}는 정답이 아닙니다.", Toast.LENGTH_SHORT).show()
            }
        }
    }
}

 

ㅇ Activity는 View와 Model을 컨트롤하는 Controller이다. Presentation 로직을 가지고 있고 화면에 출력을 할 수 있는 역할을 한다. 

 

 

 

 

 

 

 

 

 

 

 

MVP 패턴 구현

 

 

 - 간단하게 퀴즈 형식의 Toast 메시지를 출력하는 방식 ( View + Model + Presentor )


Model 영역
package com.example.mvcmvpmvvmpattern.mvp.model

data class Answer(
    var result : String? = null
){
    fun get_result(result: String?) : Boolean{
        if(result == ans){
            this.result = result
            return true
        }
        else{
            this.result = result
            return false
        }
    }

    companion object{
        const val ans = "베이징"
    }
}

 

Presentor 영역
package com.example.mvcmvpmvvmpattern.mvp.presentor

import com.example.mvcmvpmvvmpattern.mvp.model.Answer

interface GetAnswer {
    val answer : Answer
    fun getAnswer()
}
package com.example.mvcmvpmvvmpattern.mvp.presentor

import com.example.mvcmvpmvvmpattern.mvp.model.Answer
import com.example.mvcmvpmvvmpattern.mvp.view.MyAnswerView

class GetAnswerImp(private val myAnswerView: MyAnswerView) : GetAnswer {
    override val answer: Answer
        get() = Answer()

    override fun getAnswer() {
        val answerName = myAnswerView.answer
        val isCollect : Boolean = answer.get_result(answerName)

        myAnswerView.getAnswercollect(isCollect)
    }
}

1. 인터페이스를 통해 작성하여서 테스트도 용이하고 결합도도 낮아 졌다.

 

 

View 영역
package com.example.mvcmvpmvvmpattern.mvp.view

interface MyAnswerView {
    val answer : String

    fun getAnswercollect(isBoolean: Boolean)
}
package com.example.mvcmvpmvvmpattern.mvp.view

import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.example.mvcmvpmvvmpattern.databinding.ActivityMvcBinding
import com.example.mvcmvpmvvmpattern.databinding.ActivityMvpBinding
import com.example.mvcmvpmvvmpattern.mvc.model.Answer
import com.example.mvcmvpmvvmpattern.mvp.presentor.GetAnswerImp

class MvpActivity : AppCompatActivity(), MyAnswerView {
    private lateinit var binding: ActivityMvpBinding

    private lateinit var getAnswerImp: GetAnswerImp
    override val answer: String
        get() = binding.editTextView.text.toString()


    override fun getAnswercollect(isBoolean: Boolean) {
        if(isBoolean){
            Toast.makeText(this, "${answer}는 정답 입니다.", Toast.LENGTH_SHORT).show()
        }
        else{
            Toast.makeText(this, "${answer}는 틀렸습니다.", Toast.LENGTH_SHORT).show()
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMvpBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)

        getAnswerImp = GetAnswerImp(this)

        binding.resultButton.setOnClickListener {
            getAnswerImp.getAnswer()
        }
    }
}

 

Activity가 온전한 View로 간주된다. Presentor가 데이터흐름을 제어함, Interface의 구축 비용이 증가 한다. 또한 1:1 관계라서 앱이 커질 수록 view와 presentor의 구현 사항 또한 증가하게 된다.

 

 

 

 

 

 

 

 

 

MVVM 패턴 구현

 

Model 영역
package com.example.mvcmvpmvvmpattern.mvvm.model

data class Answer(
    var result : String? = null
){
    fun get_result(result: String?) : Boolean{
        if(result == ans){
            this.result = result
            return true
        }
        else{
            this.result = result
            return false
        }
    }

    companion object{
        const val ans = "워싱턴"
    }
}

 

View 영역
package com.example.mvcmvpmvvmpattern.mvvm.view

import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import com.example.mvcmvpmvvmpattern.databinding.ActivityMvvmBinding
import com.example.mvcmvpmvvmpattern.mvc.model.Answer
import com.example.mvcmvpmvvmpattern.mvvm.viewmodel.GetAnswerViewModel

class MvvmActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMvvmBinding
    private lateinit var getAnswerViewModel: GetAnswerViewModel
    private lateinit var answer: Answer

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMvvmBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)

        binding.resultButton.setOnClickListener {
            val resultAnswer = binding.editTextView.text.toString()
            getAnswerViewModel.getResult(resultAnswer)
        }

        getAnswerViewModel = ViewModelProvider(this).get(GetAnswerViewModel::class.java)
        getAnswerViewModel.isSuccessFlag.observe(this, loginObserver)
    }

    private val loginObserver = Observer<Boolean> { successful ->
        if (successful) {
            Toast.makeText(this, "${getAnswerViewModel.result} 정답 입니다.", Toast.LENGTH_SHORT).show()
        } else {
            Toast.makeText(this, "${getAnswerViewModel.result} 정답이 아닙니다.", Toast.LENGTH_SHORT).show()
        }
    }

}

사용자에게 보여지는 UI파트 데이터 바인딩을 통해 Viewmodel로 부터 일방적으로 통지 받은 내용을 출력하는 역할 여기서 핵심은 Observer패턴 구조이다. 즉 관찰 하기 때문에 데이터의 변화에 있어 능동적으로 대응할 수 있다.

 

 

ViewMdoel 영역
package com.example.mvcmvpmvvmpattern.mvvm.viewmodel

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.mvcmvpmvvmpattern.mvvm.model.Answer

class GetAnswerViewModel : ViewModel() {
    private val answer = Answer()
    private val _isSuccessFlag : MutableLiveData<Boolean> = MutableLiveData()

    val isSuccessFlag : LiveData<Boolean>
        get() = _isSuccessFlag
    val result : String
        get() = answer.result.toString()

    private fun setSuccessfulFlag(isSuccessFlag: Boolean) {
        _isSuccessFlag.postValue(isSuccessFlag)
    }

    fun getResult(result:String) {
        val isLoginSuccessful: Boolean = answer.get_result(result)
        if (isLoginSuccessful) {
            setSuccessfulFlag(true)
        } else {
            setSuccessfulFlag(false)
        }
    }

}

view를 만드는데 필요한 로직이 담긴 모델 View를 참조하지 않기 때문에 View와 Viewmodel이 N:1로 구성된다.

'안드로이드 > 정리(Android)' 카테고리의 다른 글

Android) WorkManager 워크매니저  (2) 2022.09.11
안드로이드 HTTP 통신  (2) 2022.09.09
Android) Databinding 정리  (0) 2022.08.16
Android - Hilt(의존성주입)  (0) 2022.08.15
[Android] ViewModel 알아보기  (0) 2022.08.04