정리하기 앞서
안드로이드 클린 아키텍처에 관해 공부 및 코드 분석을 진행하던 중 궁금한 부분이 생겼다. UI와 ViewModel의 상태를 data class와 MutableStateFlow를 겹합하여 상태를 관리하는 구조를 확인할 수 있었다.
Data class는 copy 함수를 제공하여 클래스의 프로퍼티들을 하나 이상을 동시에 업데이트하면서 업데이트 하지 않는 나머지 값은 보존할 수 있는 기능을 제공한다.
이 때, 구글 안드로이트 팀에서 제공하는 클린 아키텍처 예제코드에서 MutableStateFlow에 결합된 data class의 프로퍼티를 value가 아닌 update 함수를 이용하는 것을 확인할 수 있었다.
data class AddEditTaskUiState(
val title: String = "",
val description: String = "",
...
)
@HiltViewModel
class AddEditTaskViewModel @Inject constructor(
private val tasksRepository: TasksRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val _uiState = MutableStateFlow(AddEditTaskUiState())
val uiState: StateFlow<AddEditTaskUiState> = _uiState.asStateFlow()
...
fun saveTask() {
if (uiState.value.title.isEmpty() || uiState.value.description.isEmpty()) {
_uiState.update {
it.copy(userMessage = R.string.empty_task_message)
}
return
}
...
}
}
왜 update 함수를 사용하는 것인지 궁금해서 찾아보게 되었고, 그 내용을 정리해보고자 한다.
어떤 문제가 있는가?
StateFlow 는 일반적으로 안드로이드에서 MVVM 패턴으로 UI 상태를 유지하고 변경하는 데 주로 사용된다. 예를 들어 뷰 상태를 설명하기 위해 데이터 클래스의 StateFlow를 표시하는 ViewModel이 있을 수 있다. 뷰의 상태는 Data class로 설명될 수 있다. 아래 예제 코드를 사용하면서 예시를 든다.
data class ViewState(
val name: String = "ChulSoo",
val age: Int = 20,
)
class MyViewModel : ViewModel() {
private val _viewState = MutableStateFlow<ViewState>(ViewState())
val viewState = _viewState.asStateFlow()
}
액티비티 또는 프레그먼트와 같은 UI에서 ViewModel와 StateFlow에 의해 업데이트 된 값들을 이용하여 UI 상태를 변경하거나 변경된 값을 업데이트할 수도 있다.
이 때 변경되는 값이 다양한 스레드에 의해 동시에 수정되면 문제가 발생할 수 있다. 즉 위의 예제의 ViewState 가 크리티컬 섹션(Critical Section)이 되는 것이다.
Thread #1에서 이름을 Minsu가 업데이트 하였다. 이름이 업데이트가 완료되지 않은 시점에 Thread #2에서 age가 22로 업데이트를 요청하였다 그럼 최종적인 값은 어떻게 될까?
Data class의 프로퍼티가 name = "Minsu", age="22"가 되는 것일까? 문제는 그렇게 정보가 업데이트 되지 않을 수 있기에 발생한다. 온전히 타이밍에 따라 어떤 값으로 최종 업데이트 될지 판단하기 어렵다.
그림으로 이해가 어렵다면 코드로 확인한다. 위의 그림엔 스레드로 표기하였지만, 코루틴을 이용한 방법이 더 이해가 쉬울 것 같아 코드는 코루틴 디스패처를 달리한 방식으로 설명한다.
viewModelScope.launch(Dispatchers.IO) {
_viewState.value = _viewState.value.copy(name = "Minsu")
}
viewModelScope.launch(Dispatchers.Default) {
_viewState.value = _viewState.value.copy(age = 22)
}
Dispatchers.IO 에서 name을 "Minsu"로 업데이트하고, age를 22로 업데이트 한다. 이런 경우 어떤 코루틴이 먼저 실행되고끝날지 타이밍 문제로 인해 예측할 수 없는 결과가 나타난다. 이러한 문제를 막아야 한다.
해결방법
#1. 동기화 기법
동기화 기법을 사용하는 방법이 있다. Critical Section에 Lock을 이용하여 데이터의 원자성을 보장한다. 아래 코드는 Mutex를 이용하여 StateFlow의 정보 업데이트 할 때 원자성을 보장하는 방법이다.
val mutex = Mutex()
viewModelScope.launch(Dispatchers.IO) {
mutex.withLock {
_viewState.value = _viewState.value.copy(name = "Minsu")
}
}
viewModelScope.launch(Dispatchers.Default) {
mutex.withLock {
_viewState.value = _viewState.value.copy(age = 22)
}
}
이 방법으로 원자성을 보장하지만 추천하지 않는다. 개발자가 ViewState에 대한 작업을 할 때마다 일일이 기억하여 동기화 처리를 해줘야하는 번거로움이 있다. 이러한 번거로움은 휴먼에러로 이어질 수 있으며 예상하지 못하는 추가적인 문제를 야기하기도 한다.
#2. MutableStateFlow의 Extentions 함수 사용하기 (since kotlin version 1.5.1)
코틀린 버전 1.5.1에서 MutableStateFlow의 확장함수가 몇 가지 추가되었다.
- getAndUpdate
- update
- updateAndGet
위 3가지 함수 중 update에 대해서만 정리하면 update 함수는 아래와 같이 설명이 되어 있다.
Updates the MutableStateFlow.value atomically using the specified function of its value.
function may be evaluated multiple times, if value is being concurrently updated.
원자적으로 값을 업데이트할 수 있다는 의미이다. 그럼 어떻게 원자성을 보장할 수 있는지 함수의 원형을 보면 아래와 같다.
public inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
while (true) {
val prevValue = value
val nextValue = function(prevValue)
if (compareAndSet(prevValue, nextValue)) {
return
}
}
}
while (true) 스코프 내에서 이전 값과 현재 값을 얻어오고 비교하고 값을 업데이트하는 것을 보장할 수 있도록 되어있다. update, updateAndGet, getAndUpdate 함수를 이용하여 원자성을 손쉽게 보장할 수 있다. 따라서 위의 예제 코드를 update 함수를 적용한다면 아래와 같이 표현할 수 있다.
viewModelScope.launch(Dispatchers.IO) {
_viewState.update { it.copy(name = "Minsu") }
}
viewModelScope.launch(Dispatchers.Default) {
_viewState.update { it.copy(age = 22) }
}
Reference
'Android > Component' 카테고리의 다른 글
[Android] Fragment 커스텀 생성자? FragmentFactory? (2) | 2022.11.25 |
---|---|
ViewModel의 DataEvent 처리 방법 #1. LiveData (0) | 2022.10.13 |
[Android] 하단 탭 사용하기 (BottomNavigation) (0) | 2022.08.31 |
[Android] WearOS HealthServicesClient 성능 문제 분석기 (0) | 2022.08.16 |
[Android] Google Map Key 관리 (0) | 2022.08.16 |