MutableStateFlow, update() 를 사용하여 안전하게 UI 상태 업데이트하기

DwEnn
6 min readApr 14, 2022

--

이전 글에서 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 = ""
)

그리고 이 UIStateA스레드에서 query 값을 변경하고 B스레드에서 isLoading 을 변경한다고 가정해보겠습니다. 이때, A스레드가 먼저 완료되어 copy 를 완료한 시점에 B스레드도 작업이 완료되어 값 변경을 완료하였다면 UIState 의 값은 전혀 의도하지 않은 값으로 변경이 되게 됩니다. 아래의 그림을 같이 보겠습니다.

  1. A스레드UIState 를 copy 하여 query 값을 변경하였습니다.
  2. B스레드UIState 를 copy 하여 isLoading 값을 변경하였습니다.
  3. B스레드MutableStateFlow.setValue() 메서드로 변경된 UIState 를 업데이트 하였습니다.
  4. 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 가 일치할때 까지 반복을 계속합니다. setValueupdate 로 바꾸어 실행해보겠습니다.

  1. A스레드UIState 를 copy 하여 query 값을 변경하였습니다.
  2. B스레드UIState 를 copy 하여 isLoading 값을 변경하였습니다.
  3. B스레드MutableStateFlow.update() 메서드로 변경된 UIState 를 업데이트 하였습니다. prevValue 가 현재 값과 동일하여 compareAndSet 이 수행되고 MutableStateFlow 의 값이 변경되었습니다.
  4. A스레드MutableStateFlow.setValue() 메서드로 변경된 UIState 를 업데이트 하였습니다. prevValue 가 현재 값과 동일하지 않습니다. (A스레드의 prevValue 는 UIState(isLoading = false, query = “”)) compareAndSet 이 수행되지 않고 반복을 계속합니다.
  5. A스레드UIState 를 copy 하여 query 값을 변경하였습니다.
  6. A스레드MutableStateFlow.update() 메서드로 변경된 UIState 를 업데이트 하였습니다. prevValue 가 현재 값과 동일하여 compareAndSet 이 수행되고 MutableStateFlow 의 값이 변경되었습니다.

📝 NOTE : 예제 코드에서 copy 를 수행하고 있기에 update() 메서드의 반복문에서 copy 가 반복되는 것처럼 보일 수 있으나, 정확히 말하면 update() 메서드의 val nextValue = function(prevValue) 라인이 실행된 것 입니다.

이제 update() 메서드를 통해 안전하게 UI 상태를 변경할 수 있습니다. 🤗

--

--