먼저 패턴
에 대해서 이해할 필요가 있다.
프로젝트를 생성하고 하나의 서비스를 개발할 때 어떤 규칙도 없이 그냥 코드를 작성할 수도 있다.
예를 들어 간단한 메모 앱을 만들 때 사용자의 입력, 입력값 저장, 불러오기 등의 모든 로직을 Activity에서 처리했다고 가정해보자.
이후에 사용자가 메모를 여러 개 작성할 수 있도록 해주세요!
라는 리뷰를 남겼고 다시 코드로 돌아가 기능을 추가하려할 때 순간 멈칫하게 될 것이다.
어라.. 여기에 이 코드를 넣으면 이것도 고치고 저것도 고치고..
다 갈아엎어야 하네..?
하는 상황이 생길 수도 있는 것이다.
여러 기능들이 분리되지 않고 코드간의 연관성이 너무 밀접해서 유지보수가 힘들어지는 이러한 문제를 해결하기 위해 나온 것이 패턴이다.
그리고 우리는 프로그램 구조를 적절히 설계하고 기능에 따라 분리해 코드를 작성하는 여러 아키텍쳐 패턴 중, MVVM을 채택하는 이유와 그 적용방법에 대해 알아볼 것이다.
MVVM 패턴은 Model, View, ViewModel을 분리해 뷰에 모델간의 의존성을 줄여주도록 한다.
안드로이드 공식문서 : 앱 아키텍처 가이드의 권장 앱 아키텍처 다이어그램
뷰(Activity / Fragment)와 모델(Repository)이 분리되어 있고, 이 분리된 두 로직 사이에서 뷰의 이벤트에 따라 모델이 데이터를 반환/저장하도록 통신하는 뷰모델이 존재한다.
그러니까 예를 들면 아래와 같은 구조인 것이다.
뷰 | 뷰모델 | 모델 |
---|---|---|
이벤트를 발생시켜 데이터 요청 | ||
해당 데이터를 불러오는 모델의 메소드를 호출 | ||
뷰모델에서 요청하는 값을 반환 | ||
모델로부터 받은 값을 라이브데이터에 저장 | ||
라이브데이터를 감지해 저장된 값을 뷰에 출력 |
이런 구조가 된다면 뷰는 모델이, 모델은 뷰가 어떻게 동작하는지와 상관없이 로직을 작성할 수 있고 뷰모델을 통해 데이터를 통신할 수 있게 된다.
패턴에 대해 이해했다면, 관심사를 분리하기 위해서 아키텍처 구조를 설계하는 방법은 여러가지가 있다는 것을 눈치챘을 것이다.
안드로이드에서 주로 사용하는 패턴은 MVC(Model, View, Controller)
, MVP(Model, View, Presenter)
등이 있고 이번 프로젝트에서는 MVVM 패턴을 적용하고 있다.
그렇다면 왜, MVVM을 선택했을까?
일단 MVC의 경우에는 안드로이드에서 적용할 때 View와 Controller가 Activity에서 모두 처리되어야하기 때문에 Activity가 커지는 문제가 있어서 관심사의 분리가 비교적 원활하지 않다고 여겨졌다.
MVP는 Presenter가 뷰와 1대1로 동작하기 때문에 뷰와 프레젠터의 의존성이 강해지는 문제가 발생하고 이에 따라 종종 프레젠터의 로직이 비대해지는 문제가 발생하기도 했다.
따라서 뷰와 모델의 관심사를 충분히 분리할 수 있고, 화면회전 등의 동작으로 뷰가 다시 생성되어도 뷰모델을 통해 데이터를 유지할 수 있는 MVVM 방식을 채택하기로 결정했다.
간단히 버튼을 누르면 데이터를 호출해 특정 텍스트뷰에 출력하는 예제에 MVVM 패턴을 적용해보도록 한다. 추가적으로 데이터바인딩까지 적용한다.
데이터바인딩을 사용하려면 앱의 build.gradle에 아래와 같이 추가해야한다.
android {
...
dataBinding {
enabled = true
}
}
뷰, 뷰모델, 모델 클래스를 생성하고 뷰에서는 뷰모델에, 뷰모델에서는 모델에 접근할 수 있도록한다.
// Activity
MainAcitivity(): AppCompatActivity(){
private val binding: ActivityMainBinding
private val mainViewModel: MainViewModel by viewmodels()
override fun onCreate(savedInstanceState: Bundle?){
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this,R.layout.activity_main)
setupObserver()
setupClickListener()
...
}
fun setupObserver(){
mainViewModel.data.observe(this, {
binding.textview.text = it
})
}
fun setupClickListener(){
binding.button.setOnClickListener{
mainViewModel.getData()
}
}
}
// ViewModel
MainViewModel(): ViewModel(){
private val mainRepository = MainRepository()
// 데이터를 캡슐화하여 외부(뷰)에서 접근할 수 없도록하고
// 외부 접근 프로퍼티는 immutable 타입으로 제한해 변경할 수 없도록 한다.
private val _data = MutableLiveData<String>("")
val data: LiveData<String> = _data
fun getData(): {
_data.value = mainRepository.getData()
}
}
// Modle
MainRepository(){
// 일반적으로 local db나 api 데이터를 호출해온다.
// 이번 예제에서는 간단히 repository의 프로퍼티 값을 호출하는 방식으로 작성해보았다.
private val data: String = "Data 호출 성공"
fun getData(): String {
return data
}
}
위의 상태에서 activity의 버튼을 누르면 뷰모델의 getData 메소드가 호출된다.
뷰모델이 모델의 getData 메소드를 호출해 반환받은 데이터를 livedata에 저장한다.
뷰에서는 livedata의 값을 observe해서 변화하면 textview에 그 값을 출력한다.
위의 코드에서 액티비티의 observe하는 코드를 생략하고 xml에서 데이터바인딩을 사용해 라이브데이터가 변경되면 값을 출력하게 할 수 있다.
아래와 같이 text와 onClick에 뷰모델 프로퍼티와 메소드를 지정할 수 있고, onClick시 뷰모델의 메소드가 호출되고, data 값이 변경되면 text값이 변경된다.
<layout 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">
<data>
<variable
name="mainViewModel"
type="com.example.sample.MainViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
... >
<TextView
android:id="@+id/textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{mainViewModel.data}"
... />
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{mainViewModel.getData()}"
... />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
데이터바인딩을 적용하면 액티비티의 코드가 아래처럼 줄어든다.
MainAcitivity(): AppCompatActivity(){
private val binding: ActivityMainBinding
private val mainViewModel: MainViewModel by viewmodels()
override fun onCreate(savedInstanceState: Bundle?){
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this,R.layout.activity_main)
...
}
}
이처럼 MVVM 패턴을 통해 뷰와 모델의 의존성을 분리하고 데이터바인딩을 적용해 레이아웃에서 값을 직접 할당하면서 액티비티의 코드를 줄일 수 있다.