이전 글에서 ViewModel 은 state holder 로써 UI State
를 UI 에게 노출하는 역할을 한다고 설명하는 것을 보았습니다. UI 에서 이벤트를 전달받으면 ViewModel 은 사용자 액션을 처리하고 UI State
를 업데이트하면 UI 에 피드백되어 렌더링되는 구조입니다.
그리고 UI State
를 변경하는 방법은 아래의 예제를 통해 확인했었습니다.
fun fetchArticles(category: String) {
fetchJob?.cancel()
fetchJob = viewModelScope.launch {
try {
val newsItems = repository.newsItemsFor(category)
_uiState.update {
it.copy(newsItems = newsItems)
}
} catch (ioe: IOException) {
// Handle the error and notify the UI when appropriate.
_uiState.update {
val messages = getMessagesFromThrowable(ioe)
it.copy(userMessages = messages)
}
}
}
}
저는 이 MutableStateFlow.update()
메서드가 무엇이며 왜 안전한것인지에 대해 얘기해보려고 합니다.
MutableStateFlow.value()
먼저, MutableStateFlow
를 안전하지 않은 방법으로 업데이트 하는 예를 살펴보겠습니다.
fun fetchArticles(category: String) {
...
_uiState.value = _uiState.value.copy(newsItems = newsItems)
...
}
MutableStateFlow.value()
를 통해 현재 UI State
데이터를 가져온 후 copy 하여 변경하고자 하는 속성만 변경해주었습니다. 간단히 상태가 업데이트 된 것 같지만 이것은 동시성 문제를 야기할 수 있습니다. MutableStateFlow.value()
는 thread-safe 하게 설계되었지만, copy 된 새 데이터를 setValue 하는 과정은 thread-safe 하지 않기 때문입니다.
아래의 예를 보며 구체적으로 어떤 동시성 문제가 발생하는지 살펴보겟습니다. 👀
먼저 UIState
를 정의하겠습니다.
data class UIState(
val isLoading: Boolean = false,
val query: String = ""
)
그리고 이 UIState
를 A스레드에서 query 값을 변경하고 B스레드에서 isLoading 을 변경한다고 가정해보겠습니다. 이때, A스레드가 먼저 완료되어 copy 를 완료한 시점에 B스레드도 작업이 완료되어 값 변경을 완료하였다면 UIState
의 값은 전혀 의도하지 않은 값으로 변경이 되게 됩니다. 아래의 그림을 같이 보겠습니다.
- A스레드가
UIState
를 copy 하여 query 값을 변경하였습니다. - B스레드가
UIState
를 copy 하여 isLoading 값을 변경하였습니다. - B스레드가
MutableStateFlow.setValue()
메서드로 변경된UIState
를 업데이트 하였습니다. - A스레드가
MutableStateFlow.setValue()
메서드로 변경된UIState
를 업데이트 하였습니다.
4번까지의 실행결과를 보면 B스레드의 상태 업데이트가 적용되지 않아 isLoading 이 false 인것을 볼 수 있습니다. 🥶
MutableStateFlow.update()
public inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
while (true) {
val prevValue = value
val nextValue = function(prevValue)
if (compareAndSet(prevValue, nextValue)) {
return
}
}
}
update()
메서드의 내부는 위와 같습니다. 내부적으로 CAS 를 이용하여 동시성 문제를 해결하고 있습니다. prevValue 가 일치할때 까지 반복을 계속합니다. setValue
를 update
로 바꾸어 실행해보겠습니다.
- A스레드가
UIState
를 copy 하여 query 값을 변경하였습니다. - B스레드가
UIState
를 copy 하여 isLoading 값을 변경하였습니다. - B스레드가
MutableStateFlow.update()
메서드로 변경된UIState
를 업데이트 하였습니다. prevValue 가 현재 값과 동일하여compareAndSet
이 수행되고 MutableStateFlow 의 값이 변경되었습니다. - A스레드가
MutableStateFlow.setValue()
메서드로 변경된UIState
를 업데이트 하였습니다. prevValue 가 현재 값과 동일하지 않습니다. (A스레드의 prevValue 는UIState(isLoading = false, query = “”)
)compareAndSet
이 수행되지 않고 반복을 계속합니다. - A스레드가
UIState
를 copy 하여 query 값을 변경하였습니다. - A스레드가
MutableStateFlow.update()
메서드로 변경된UIState
를 업데이트 하였습니다. prevValue 가 현재 값과 동일하여compareAndSet
이 수행되고 MutableStateFlow 의 값이 변경되었습니다.
📝 NOTE : 예제 코드에서 copy 를 수행하고 있기에
update()
메서드의 반복문에서 copy 가 반복되는 것처럼 보일 수 있으나, 정확히 말하면 update() 메서드의val nextValue = function(prevValue)
라인이 실행된 것 입니다.
이제 update()
메서드를 통해 안전하게 UI 상태를 변경할 수 있습니다. 🤗