첫 주제는 역시 MVVM이다. 프로젝트 진행 전에 MVVM을 공부했을 땐
왜 MVVM을 적용해야 되는지 잘 와닿지 않았다.
지금 누군가 나에게 MVVM을 적용해야 하는지에 대한 이유를 하나만 대라고 하면,
나는 역할에 따른 코드 분리라고 말하고 싶다.
안드로이드의 MVVM 공식 문서에서도 관심사 분리라는 키워드가 등장한다.
MVVM을 적용하지 않은 코드, 아마 MVC 아키텍처를 적용한 프로젝트의 코드라면
뷰 이벤트 처리 코드, 데이터 처리, 데이터 갱신 코드가 모두 Activity/Fragment 파일에 기술된다.
이해하기 쉽도록 각 역할에 따른 코드가 150줄씩 있다고 생각해보자.
Activity/Fragment 파일에 300줄, (역할에 따라 코드를 분리할 수 있음에도) 코드의 양이 비대해질 것이다.
현업에서 150, 300줄은 장난 수준일 듯
단순 코드의 양만 비대해지는 것이 아니다.
각기 다른 역할의 코드들이 한 클래스에 모여 있다.
MVC 아키텍처와 MVVM 아키텍처를 모두 사용해 프로젝트를 진행해보니
위의 문제점 해결이 가장 크게 느껴졌다.
우리가 코드를 기술하는 Activity 등의 파일은 어쩌면 개발자들에겐 작업 환경이라고도 할 수 있다.
일에 있어 어떤 작업 환경이 일관성이 없고, 무질서하다면?
현재 내가 하고 있는 작업, 또는 추후 내가 해야 할 작업 모두 힘들어질 것이다.
남이 써놓은 코드를 내가 읽어야하는 상황이라도 해도 같을 것이다.
두 클래스 중 어느 클래스의 코드가 읽기 쉬울까?
추후 어떤 기능 추가 시, 어떤 클래스가 코드를 작성하기 더 쉬울까?
당연히 후자일 것이다.
OOP를 공부해봤다면 알 것이다.
메소드는 하나의 작업만 수행하는 것이 중요하고
클래스는 객체 지향 관점에서 하나의 객체를 나타내는 것이 중요하다.
그것과 동일한 이유라고 생각한다.
MVVM는 '역할에 따른 명확한 클래스 분리'라는 OOP의 가장 중요한 원칙과 원리적으로 동일 선상에 있다고 생각한다.
여기까지가 이번 프로젝트에서 MVVM을 적용해본 결과,
MVVM 적용 시 얻을 수 있는 가장 핵심적인 이점이라고 생각한다.
실제 Android 공식 문서에서도, MVVM 구현의 가장 중요한 원칙을 '관심사 분리'로 소개한다.
물론 View와 Model을 분리함에 따라 생기는 다른 이점들도 많지만,
핵심적인 내용을 중심으로 다루고 싶었기에 해당 내용은 아래 섹션에서 추가적으로 설명하겠다.
ViewModel
ViewModel은 뷰에게 데이터를 제공해주는 관찰자 클래스이다. ViewModel은 View(Activity/Fragment)의 LifeCycle을 인지한다. 수명주기를 인지하는만큼 높은 유연성을 자랑한다. 연결된 수명 주기가 끝나면(destroy) 자동으로 삭제되기도 한다. 좀 더 살펴보자. 결국 ViewModel의 역할은 View 업데이트용 데이터를 가지고 있는 역할이다. 따라서 View의 LifeCycle이 활성화돼 있을 때만 ViewModel 인스턴스가 활성화되면 된다.근데 이 바람대로, ViewModel은 이에 부합하다는 것이다. 따라서 View가 활성화돼있는 순간엔 ViewModel도 자동으로 함께하고 있고, 앞서 언급했 듯 View가 소멸되는 순간에 같이 소멸되기도 한다는 것이다. ViewModel은 생명주기와 유연하게, 부드럽게 동행한다.
또, ViewModel의 LiveData를 빼놓고 설명할 수 없다. LiveData는 '데이터 홀더 클래스'이다. 데이터를 홀더하고 있기 때문에 네트워크 연결이 취약하거나 연결되어 있지 않더라도 앱의 정상 작동이 가능하다. 물론 마지막으로 업데이트된 홀더 데이터를 바탕으로 View가 업데이트된다. 또한 앱 데이터가 변경될 때 관찰자가 대신 UI를 업데이트해주므로, 개발자가 일일이 UI를 업데이트해주지 않아도 된다는 장점이 있다. 화면 전환 시에도 괜찮다. View의 수명 주기 상태가 변화하더라도 LiveData는 최신의 데이터로 View를 자동 업데이트 해주고 있다.
이러한 사항들은 결과적으로 Memory Leak(메모리 누수) 방지로 이어진다. 먼저 View는 Model에 직접 접근하지 않기 때문에, LifeCycle 의존도가 낮아 진다는 이점을 가진다. Model 접근은 ViewModel이 대신한다. 그리고 이 ViewModel의 LiveData는 View가 활성화돼있을 때만 동작하여 Memory Leak(불필요한 메모리 점유)을 방지 할 수 있다는 것이다. 지금까지 ViewModel이 LifeCycle을 인지하므로 얻게 되는 이점들에 대해 주력으로 소개했다. 그리고 다음과 같은 이점들도 존재한다.
- View와 Model이 서로 전혀 알지 못하기에 독립성을 유지할 수 있다.
- 독립성을 유지하기 때문에 효율적인 유닛테스트가 가능하다.
- View와 ViewModel을 바인딩하기 때문에 코드의 양이 줄어든다.
MVVM이 장점을 가지고 있다는 것도 알겠다.
그렇다면 어떻게 구현하는지도 큰 관심사일 것이다.
MVVM을 정리한 문구를 살펴보자.
데이터와 관련된 변수들은 ViewModel에서 LiveData로 관리하고
Observe(관찰)를 통해 xml로 전달 받아 뷰를 업데이트 한다.
유저 액션(클릭 이벤트)가 발생 했을땐 ViewModel에서 작성한 함수를 호출하는 방식
으로 코드를 구분하고 관리한다.
간단하게 정리하자면
ViewModel이라는 녀석이 핵심적인 역할을 하는 듯 하다.
ViewModel은 데이터에 관련된 변수를 관리하고, 데이터 변경을 관찰하고, 유저 액션에 따른 함수를 호출해준다.
필자에게만 국한된 것일 수도 있지만 이런 설명들을 보고 전혀 감이 오지 않았다.
MVVM에서 기본적으로 세 가지 클래스가 필요하다.
따라서 ViewModel이라는 녀석을 사용해서 MainActivity 코드를 작성하고 싶다면, 아래 세 가지의 클래스가 필요하다.
정직하게, 세 개의 클래스를 새로 만들었다.
필자는 지금 TextView, EditText, Button이 있는 앱을 만들고 싶다.
TextView엔 DB의 최신 데이터가 실시간으로 업데이트된다.
Button을 클릭하면 editText의 text로 서버의 데이터 값을 업데이트하는 기능을 만들 것이다.
MVVM의 핵심인 ViewModel 클래스부터 작성해보자. ViewModel 클래스엔
(0) Repository 클래스 인스턴스
(1) 데이터에 관련된 변수들
(2) 데이터 변경을 관찰
(3) 뷰에서 발생한 유저 액션에 따라 호출되어야 하는 함수
에 대한 코드들을 작성해주면 된다.
<<ViewModel.kt>>
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class ViewModel () : ViewModel(){
// (0)
private val repository : Repository = Repository()
// (1)
private var _data = MutableLiveData<String>()
val data : LiveData<String> = _data
// (2)
init {
repository.listenMyData(_data)
}
// (3)
fun updateData(newData : String){
repository.updateData(newData)
}
}
(0) Repository 클래스 인스턴스
ViewModel은 Repository 클래스 인스턴스를 가지고 있다.
왜일까? Repository 클래스가 서버의 데이터를 create, read, update, delete 할 수 있는 클래스니까.
이 Repository 클래스 인스턴스를 통해 '데이터 변경 감지' 및 '데이터 처리' 등의 작업을 수행한다.
Activity 클래스와 Repository 클래스 사이에 위치하는 ViewModel 클래스는,
Repository 클래스 인스턴스를 가지고 있는 것이 당연한 것이다.
(1) 데이터에 관련된 변수
LiveData는 수명주기를 인식하는, 관찰 가능한 데이터 홀더 클래스이다.
ViewModel 클래스는 데이터 변경을 감지한다고 했다. 이것이 '관찰'이다.
해당 LiveData type의 변수들은, Repository 로부터 데이터 변경을 감지할 수 있다.
데이터 변경을 감지하게 되면, 변경된 데이터로부터 Activity 클래스의 View를 업데이트해주는데,
이에 대한 코드는 Activity에서 작성하니, 조금만 기다려보자.
(1+) MutableLiveData<String>, LiveData<String> 뭔 차이죠?
MutableLiveData<String> : private(외부 클래스에서 접근 불가능), var-mutable(값 수정 가능)
LiveData<String> : public(외부 클래스에서 접근 가능), val(값 수정 불가능)
결과적으론 같은 값을 가지고 있는 두 변수이다. 하지만, 두 가지 역할로써 두 가지 변수가 필요하다.
- Repository 클래스 메소드를 통해 변경된 데이터 값으로 업데이트 되는 LiveData 변수
- 값이 변경 가능해야 하고(var-mutable) 다른 클래스에서 마음대로 접근해 값을 변경하면 안 된다.(private)
- View를 업데이트하기 위해 Activity에서 접근할 수 있는 LiveData 변수
- 외부에서 접근이 가능해야 하고(public) 마음대로 값을 수정할 수 없도록 해야한다.(val)
(2) 데이터 변경을 관찰
ViewModel 클래스는 관찰 가능한 변수를 가지고 있다.
그렇다면, 데이터 변경이 발생하면 해당 변수(데이터)의 값이 변경되야 하는 것은 당연한 것이 아닌가?
해당 기능은 당연하게도, 데이터를 read할 수 있는 Repository 클래스의 메소드를 사용해 데이터 값을 변경시켜준다.
(2+) init scope 내에 구현된 이유?
ViewModel 인스턴스가 생성된 순간부터, Repository를 통해 데이터 변경 감지를 시작하는 것이다.
이후 데이터 변경이 발생하면, Repository로부터 LiveData 데이터 값을 변경한다.
그리고 Activity에선 이 변경된 LiveData 데이터 값으로부터 View를 업데이트한다.
(3) 뷰에서 발생한 유저 액션에 따라 호출되어야 하는 함수
Activity 에 editText 와 Button 을 구현할 것이고,
Button 을 클릭하면 editText 의 text 로 서버의 기존 값을 업데이트하고 싶다고 가정하자.
해당 함수는, 뷰에서 발생한 유저 액션(Button 클릭)에 따라 호출되어야 하는 함수(updateData)이다.
해당 함수를 통해, 함수명 이름 그대로 Repository(저장소)의 값을 변경할 것이다.
위의 listenMyData(), updateData()는, ViewModel 에서 Repository 클래스의 메소드를 사용하기 위한
인터페이스일 뿐이다. 걱정하지 말고 Repository 클래스 구현 사항을 이어서 보자.
다음은 Repository 클래스이다.
Repository엔 데이터 처리에 관한 코드들이 작성되어 있다.
데이터를 가지고 있는 것이 Repository(저장소), 직역하면 저장소 클래스.
저장소 클래스를 사용하여 데이터를 읽어 오고, 데이터를 수정하고 하는 것이 당연한 것이다.
앞서 ViewModel 클래스에서 Repository 클래스의 listenMyData(), updateData()를 사용한 것이 기억나는가?
당연히 여기에 그 메소드들이 구현되어 있다. 기존 MVC 패턴이었다면, 데이터를 처리하는 Repository 클래스의 코드들이
Activity 클래스에 기술되어 있었을 것이다. MVVM의 관심사 분리 원칙에 따라,
해당 Activity에 기술돼있던 데이터 처리 코드들이 Repository 클래스로 이전해온 것, 그 이상 그 이하도 아니다!
그렇다면 Repository 클래스를 작성해보자. 작성할 것도 없다.
(0) 실질적으로 데이터를 저장하고 있는 클래스의 인스턴스(Database 등)
(1) 데이터 처리 함수들
에 대한 코드들을 작성해주면 된다.
<<Repository.kt>>
import androidx.lifecycle.MutableLiveData
class Repository {
// (0)
private val db = Database
// (1)
fun listenMyData(_data : MutableLiveData<String>){
db.document("myData")
.listen()
.addSnapshotListener { newData ->
_data.value = newData.toString()
}
}
fun updateData(newData : String){
db.document("myData")
.update(newData)
}
}
(0) 실질적으로 데이터를 저장하고 있는 클래스의 인스턴스(Database 등)
db는 말 그대로 우리가 사용하는 어느 Database의 인스턴스이고,
db.document("myData")는 db에서 "myData"라는 네임을 가진 문서를 의미한다고 생각하면 된다.
(1) 데이터 처리 함수들
최대한 코드를 간단하게 기술했다.
listenMyData() : 저장소의 "myData" 문서에 데이터 변경이 발생하면, MutableLiveData 변수 data에
해당 값을 넣어준다. 변경된 MutableLiveData 변수의 값은, Activity까지 전달되어,
새로운 값으로 View를 업데이트할 수 있게 되는 것이다.
updateData() : 저장소의 "myData" 문서의 값을 새로운 값 newData로 update해준다.
추가로, 값이 update되면, listenMyData() 함수가 이 데이터 변경을 감지하여, 결국은 View가 업데이트될 것이다.
Activity 클래스에선 '관찰 가능한 ViewModel의 홀더 클래스 데이터(LiveData)'를 통해 View(UI)를 업데이트한다.
외부 클래스에서 접근하여 값을 읽어들이기 위한 val data : LiveData<String> 가 기억나는가?
"Activity는 해당 값의 변경을 관찰한다."
그렇다면 작업 흐름이 어떻게 되는가?
ViewModel은 Repository의 데이터 변경을 감지하고, Activity는 ViewModel의 데이터 변경을 감지하여 뷰를 업데이트한다.
Activity.kt -(관찰)-> ViewModel.kt -(관찰)-> Repository.kt의 작업 흐름이 발생하고 있는 것이다.
위의 '기본 구성' 파트의 MVVM 구조에 대한 사진을 보고 오자.
Activity/Fragment -> ViewModel -> Repository 순으로 화살표 방향 처리가 되어있다.
해당 흐름만 기억하면, MVVM을 사용할 준비는 다 되었다고 해도 과언이 아니라고 생각한다.
따라서 MainActivity엔 다음과 같은 코드들이 구현돼야 할 것이다.
(0) ViewModel 클래스 인스턴스
(1) LiveData의 데이터 관찰하여, 데이터 변경이 발생하면 새로운 값으로 TextView의 text 값을 수정하는 코드
(2) 버튼 클릭 시, editText의 text 값으로부터 db의 값을 update하는 코드
<<activity_main.xml>>
textView, editText, button. 세 가지 뷰가 있다고 상상하자!
textView : db의 myData 값을 표시
editText, button : button을 클릭하면 editText의 text 값으로 db의 값 수정(update)
<<MainActivity.kt>>
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// (0)
val viewModel: ViewModel by viewModels()
// (1)
viewModel.data.observe(this, Observer<String>{ curData ->
textView.text = curData.value
})
// (2)
button.setOnClickListener{
viewModel.updateData(editText.text.toString())
}
}
}
(0) Activity 클래스는 "Repository에 직접 접근하는 것이 아니라, ViewModel을 통해 간접 접근한다.
따라서 Activity가 ViewModel 클래스 인스턴스를 가지고 있는 것은 당연하다!
(1)
Activity는 ViewModel의 LiveData 변수인 data의 데이터 변경을 감지한다. 따라서 observe(관찰) 메소드를 사용한다.
해당 observe 메소드는 (해당 데이터의 값 변경 시 그 값을 가져오는) CallBack 메소드이다. 따라서 구현 사항을 구현해주면 된다.
data 변수의 데이터 값이 변경되면 뭘하기로 했는가? TextView의 text값을 업데이트해주기로 했다. 해당 코드를 기술해주면 된다. 쉽다.
(2)
마찬가지다. Activity는 Repository에 직접 접근을 하지 않는다. ViewModel의 메소드를 사용한다.
위 ViewModel 클래스의 '(3) 뷰에서 발생한 유저 액션에 따라 호출되어야 하는 함수'인 updateData()가 기억나는가?
해당 메소드를 그대로 사용하면 된다! ViewModel을 거쳐 Repository의 updateData()를 다시 호출하여,
데이터베이스의 값을 변경해줄 것이다.
또한 데이터 변경을 감지하고 있기 때문에(listenMyData()) 버튼을 누름과 동시에
값이 refresh 될 것임을 기억하자. 이것이 MVVM이다.