- MVVM 패턴은 Martin Fowler의 MVP 패턴의 변형이다. Microsoft의 엔지니어인 Ken Cooper와 Ted Peters에 의해 만들어졌다. 이 패턴은 Windows의 그래픽 프레임워크인 WPF와 Silverlight에서 처음 적용되었다. Microsoft에서 WPF 및 Silverlight 설계자로 일하고 있는 John Gossman은 2005년에 자신의 블로그에서 MVVM(Model-View-ViewModel) 패턴을 발표했습니다.
- 이 패턴의 주요 목적은 To simplify event-driven programming user interfaces인데, 이것을 위해 View에 관한 로직과 비즈니스 로직을 철저히 구분한다. 여기서 사용되는 주요한 개념이 Data binding이다.
> 참고
ViewModel은 추상화된 View라고 볼 수 있다. View의 추상화는 재사용 할 수 있고 테스트하기 쉽다.
ViewModel은 뷰를 추상화하기 위해 추상화된 뷰 상태(ViewState)를 유지한다. 예를 들어 뷰모델은 읽기와 쓰기가 가능한 문자열 속성을 통해 텍스트 입력기 컨트롤을 추상화한다. 데이터 목록을 보여주는 컨트롤에 대해서는 각 요소의 뷰 상태가 들어있는 컬렉션이 사용된다.
모델이 제공하는 정보가 사용자에게 전달될 때, 혹은 그 반대의 경우 원본값이 그대로 사용되기도 하지만 그렇지 않은 경우도 있다. 모델이 표준시 기준 날짜 정보를 제공할 때 이것을 그대로 노출하는 것은 사용자에게 달갑지 않은 일이다. 지역 시간대 및 현지 언어가 적용되거나 ‘어제’와 같은 상대적 표현으로 변환된다면 사용자는 더 나은 경험을 하게된다. 이런 변환 작업을 위해 뷰모델은 값 변환기(ValueConverters)를 가진다.
뷰는 자신이 가진 상태를 사용자에게 표현할 뿐 아니라 사용자가 응용프로그램에 명령을 내릴 수단을 제공한다. 뷰모델은 이런 기능을 추상화하기 위해 명령(Commands)을 가진다. 명령을 통해 사용자는 모델의 행위를 실행할 수 있다.
> 참고
- ViewState
ViewState는 화면이 가지는 본질적인 데이터입니다.
뷰 모델에서 ViewState는 뷰에 의해 입력받기도 하고, ViewState에 따라 뷰를 그려주기도 합니다.
예를 들어 로그인 화면이 있다고 가정해보겠습니다. 이 화면에서 EditText로 구성된 아이디, 비밀번호가 있습니다.
여기서 아이디, 비밀번호가 ViewState가 되는 것이고 사용자로부터 입력받아 뷰 모델에 채워지게 됩니다.- ValueConverter
ValueConverter는 본질적인 데이터들을 가지고 만들어 내는 작업입니다.
다시 로그인 화면을 예로 들어보자면, ViewState인 아이디와 비밀번호가 있겠고 활성화 시킬 수 있는 로그인 버튼이 존재합니다.
이때 로그인 버튼의 활성화 여부가 ValueConverter에 해당이 됩니다.
ViewState인 아이디, 비밀번호가 둘다 공백이 아니어야 하는 조합으로 하나의 불리언 값을 가지게 되고 뷰에 반영되게 됩니다.
코드를 작성할 때 따라서 ViewState로 작성하다가도 ValueConverter으로 표현할 수 있는 형식이 아닌지 고민해봐야 합니다.
ViewState가 적을수록 가독성이 높아지고 모듈화 하기 쉬워지기 때문입니다.- Command
뷰는 사용자에게 정보를 보여주는 역할이기도 하면서, 사용자로부터 이벤트를 받아들이는 역할도 합니다.
따라서 뷰모델은 이러한 이벤트에 대한 처리도 해줄 수 있어야 합니다. 그게 바로 Command입니다.
다시 로그인 화면을 예제로 들자면 로그인 동작이 Command가 될 것입니다.
Command은 항상 ViewState를 활용해 ViewState를 변경하거나 신호를 내보내는 역할을 합니다.
따라서 뷰 모델이 노출한 뷰의 행동은 직접적으로 뷰를 참조하지 않아도 됩니다.
> 참고- MVVM에서는 패턴 구현을 위해 Command 패턴과 Data Binding을 적극적으로 사용한다.Command 패턴은 뷰에서 UI를 처리하기 위해 뷰 모델에 전달하는 요구 사항을 캡슐화한 것이며, Data Binding은 뷰와 뷰 모델 사이의 데이터를 동기화시키는 것이라고 볼 수 있다.
> Command Pattern
Model View ViewModel 패턴은 MVP(Model View Presenter) 패턴에서 Presenter가 View 참조하고 있는 문제를 개선하기위해 뷰를 위한 모델 ViewModel은 View를 참조하고 있지않아 독립적이며 의존성을 낮춘 패턴이다.
앱에 사용되는 데이터를 관리 담당하는 역할을 합니다. 흔히 '비즈니스 로직'이리고 부르는 부분입니다. 모델에는 network API, 데이터 캐싱, 데이터베이스 등이 포함되고 Repository pattern을 사용하는 경우 Repository도 포함됩니다.
사용자 인터페이스 영역이며, Activity, Fragment 등이 포함되고 데이터를 표시하는 역할만 합니다. 오직 ViewModel을 통해서 데이터를 요청하고 ViewModel을 관찰하고 있어 변경된 데이터를 View에 반영합니다. (Observer 패턴)
ViewModel을 참조하고 있어 ViewModel에 의존적이고 ViewModel의 데이터를 소비하는 소비자라고 할 수 있습니다.
View에서 사용자 이벤트를 전달 받아 Model에 데이터를 요청하고 ViewModel 자신의 상태를 업데이트 한다. MVP의 Presenter에서는 View의 참조를 가지고 있고 View를 호출해 데이터를 전달해주었다면 ViewModel은 View를 전혀 모르고 View가 없어 독립적이며 재사용할 수 있고 View없이 테스트를 작성할 수 있습니다.
Presenter와 View는 1:1 관계이면
ViewModel과 View는 1:n 상황에 따라 n:n 구성도 가능합니다.
MVVM의 ViewModel은 안드로이드 AAC ViewModel과 다른 것!
AAC의 ViewModel은 안드로이드 플랫폼에 맞게 Lifecycle 관리를 도와주는 기능을 추가한 것 입니다.
안드로이드에서 MVVM 패턴을 구현하기 위해 여러가지 방법과 라이브러리가 있습니다.
Data binding 사용
데이터 바인딩은 데이터 소스를 소비자와 연결하고 동기화 상태를 유지하는 기술입니다. Android의 Data binding 는 programmatic 방식이 아니라 선언적 형식으로 레이아웃의 UI 구성요소를 앱의 데이터 소스와 결합할 수 있는 지원 라이브러리입니다.
Observer 패턴을 구현하기위해
위의 라이브러리를 사용한 MVVM 예제들은 많이 찾아 볼 수 있어서 저는 라이브러리를 사용하지 않고 MVVM을 구현하는 간단한 예제를 살펴보겠습니다.
참고
class MvvmActivity : AppCompatActivity() {
private val viewModel = LoginViewModel(LoginInteractor())
private val username: EditText by lazy {
findViewById(R.id.username)
}
private val password: EditText by lazy {
findViewById(R.id.password)
}
private val button: Button by lazy {
findViewById(R.id.button)
}
private val progress: ProgressBar by lazy {
findViewById(R.id.progress)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_mvvm)
viewModel.stateObservable.addObserver(::updateUI)
button.setOnClickListener {
viewModel.onLoginClicked(username.text.toString(), password.text.toString())
}
}
private fun updateUI(screenState: ScreenState<LoginState>) {
when (screenState) {
ScreenState.Loading -> progress.isVisible = true
is ScreenState.Render -> processLoginState(screenState.renderState)
}
}
private fun processLoginState(renderState: LoginState) {
progress.isVisible = false
when (renderState) {
LoginState.Success -> startActivity(Intent(this, MainActivity::class.java))
LoginState.WrongUserName -> username.error = getString(R.string.username_error)
LoginState.WrongPassword -> password.error = getString(R.string.password_error)
}
}
}
class LoginInteractor {
interface OnLoginFinishedListener {
fun onUsernameError()
fun onPasswordError()
fun onSuccess()
}
fun login(username: String, password: String, listener: OnLoginFinishedListener) {
// Mock login. I'm creating a handler to delay the answer a couple of seconds
postDelayed(2000) {
when {
username.isEmpty() -> listener.onUsernameError()
password.isEmpty() -> listener.onPasswordError()
else -> listener.onSuccess()
}
}
}
}
fun postDelayed(delayMillis: Long, task: () -> Unit) {
Handler(Looper.getMainLooper()).postDelayed(task, delayMillis)
}
class LoginViewModel(
private val loginInteractor: LoginInteractor
): LoginInteractor.OnLoginFinishedListener {
// view에서 관찰하고 있는 상태 값
val stateObservable = Observable<ScreenState<LoginState>>()
fun onLoginClicked(username: String, password: String) {
stateObservable.callObservers(ScreenState.Loading)
loginInteractor.login(username, password, this)
}
override fun onUsernameError() {
stateObservable.callObservers(ScreenState.Render(LoginState.WrongUserName))
}
override fun onPasswordError() {
stateObservable.callObservers(ScreenState.Render(LoginState.WrongPassword))
}
override fun onSuccess() {
stateObservable.callObservers(ScreenState.Render(LoginState.Success))
}
fun onDestroy() {
stateObservable.clearObservers()
}
}
class Observable<T> {
private var observers = emptyList<(T) -> Unit>()
fun addObserver(observer: (T) -> Unit) {
observers += observer
}
fun clearObservers() {
observers = emptyList()
}
fun callObservers(newValue: T) {
observers.forEach { it(newValue) }
}
}
sealed class ScreenState<out T> {
object Loading : ScreenState<Nothing>()
class Render<T>(val renderState: T) : ScreenState<T>()
}
enum class LoginState {
Success, WrongUserName, WrongPassword
}
이름과 비밀번호 입력필드와 로그인 버튼이 있고 버튼을 클릭했을때 로딩 상태로 변경하고 Model에서 이름과 비밀번호의 값이 비어있는지 체크 후
이름을 입력안한 상태, 비밀번호를 입력안한 상태, 성공 상태를 ViewModel로 return하여 View에 상태값에 따라 View를 적절히 변경해주는 예제입니다.
View에서 ViewModel의 상태를 관찰하기 위해 Observable라는 Wrapper클래스를 만들고, ViewModel에서 Observable 프로퍼티를 여러개로 구현 할 수도 있지만, State로 상태값을 지정해 프로퍼티 한 개로 통일되었습니다.
끝으로 안드로이드 MVVM 패턴을 검색하면 LiveData와 AAC ViewModel을 사용한 예제들은 많아서 안로이드 컴포넌트 없이 MVVM을 구현하는 방법을 소개하고싶어 포스트를 작성하게 되었습니다. 여러 자료를 찾아보면서 몰랐던 부분도 새로 알게되었고 MVVM에 대해 깊게 알게된 계기가 되었습니다. 안드로이드 특성상 Lifecycle을 고려해야 하기때문에 AAC ViewModel을 사용하는게 좋다고 생각합니다.