[Android] StateFlow의 원자성 보장 방법 (feat. update)
StateFlow의 값 갱신 방법
Flow의 값을 갱신할 때 사용되는 emit, value, update 세 가지 방법은 각각 고유한 특징과 차이점이 있습니다. 이들을 구분하여 이해하면 더 효율적으로 Flow를 활용할 수 있습니다.
1. emit
- emit은 Flow에서 값을 갱신하고 방출하는 가장 기본적인 방법입니다. emit은 suspend 함수로, Flow의 생산자에서 값을 방출할 때 사용됩니다.
2. value
- value는 Flow의 MutableStateFlow와 같은 상태 관리 객체에서 사용하는 프로퍼티입니다. 이 프로퍼티를 통해 값을 동기적으로 변경하고 접근할 수 있습니다.
- emit과 기능적으로 비슷하지만, value는 코루틴 외부에서도 동기식으로 호출할 수 있다는 차이가 있습니다. 예를 들어, value = "sooyeol"처럼 일반적인 변수처럼 사용이 가능합니다.
3. update
- update는 StateFlow와 같은 상태 객체에서 값을 업데이트할 때 사용되는 메서드입니다. update는 기존 객체의 프로퍼티 중 변경된 부분만을 갱신하고, 해당 객체를 방출합니다.
- emit이나 value는 기존 객체를 변경하지 않고 새로운 객체를 생성한 뒤 값을 비교하고 방출합니다. 반면, update는 기존 객체의 일부 프로퍼티만을 갱신하여 방출하는 방식입니다.
이 세 가지 방법은 각각 상황에 맞게 사용하면 됩니다. emit은 비동기적인 값 방출을, value는 동기적인 값 설정을, update는 상태의 일부만 갱신해야 할 때 유용합니다.
StateFlow의 원자성
안드로이드에서 UI에 상태를 유지하고 변경할 때, StateFlow를 많이 사용합니다. 예를 들어, 아래와 같이 data class로 생성된 상태 홀더를 StateFlow로 관리할 수 있습니다.
data class UiState(
val name: String = "",
val checked: Boolean = false
)
@HiltViewModel
class UiExampleViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
) : ViewModel() {
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<CheckUiState> = _uiState
}
그리고 상태값을 변경할 때 아래와 같이 data class의 copy 함수를 통해 변경하고자 하는 프로퍼티만 편하게 변경할 수 있습니다. 상태를 관리할 때 data class를 사용하면 한 곳에서 관련된 데이터들을 확인할 수 있고, 코드 가독성이 좋아집니다. 또한, 아래 코드 예제처럼 하나 이상의 프로퍼티를 쉽게 수정할 수 있어서 UI 상태를 간단하게 업데이트할 수 있게 됩니다.
_uiState.value = _uiState.value.copy(
name = "my name",
checked = _uiState.value.checked.not(),
)
_uiState.value = _uiState.value.copy(
name = "brother's name"
)
StateFlow의 value를 통해 업데이트할 때의 문제점
위의 예제만 보면 문제가 없는 것처럼 보이지만, 치명적인 문제가 한 가지 있습니다. 바로 다중 스레드에서 uiState의 값을 변경할 때 문제가 발생합니다. 아래 코드처럼 StateFlow의 값이 경신되기 전에 다른 스레드에서 값을 업데이트하면 원자성을 보장할 수 없게 됩니다.
viewModelScope.launch(Dispatchers.IO) {
_uiState.value = _uiState.value.copy(
name = "my name"
)
}
viewModelScope.launch(Dispatchers.Default) {
_uiState.value = _uiState.value.copy(
checked = _uiState.value.checked2.not(),
)
}
버튼을 눌렀을 때, 위의 코드가 실행되게 한다면 name은 "my name"이 되고 checked는 토글로 작동되는 것을 예상할 수 있습니다. 그러나 실제로 10~20번 버튼을 눌러보면 원자성이 보장되지 않는 결과를 확인할 수 있습니다. 즉, 위 예제의 launch 코드가 운이 좋아서 잘 작동했던 것뿐입니다.
StateFlow의 원자성을 보장하는 방법
다중 스레드에서 동시에 상태를 업데이트해도 원자성을 보장하기 위해서는 몇 가지 방법이 있는데, 여기서는 가장 쉬운 방법에 대해 설명하겠습니다. 바로, 코틀린 코루틴 1.5.1에 추가된 update() 함수를 사용하는 것입니다. 아래는 MutableStateFlow의 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
}
}
}
이전 상태값과 현재 상태값을 비교해서 다른 스레드에서 수정이 됐다면 다시 루프를 수행하는 구조로 되어있습니다. compareAndSet 함수의 내부 모습을 확인해 보면 synchronized를 이용해서 원자성을 보장하고 있습니다.
아래 코드는 아까 작성했던 예제 코드를 update() 함수를 사용해서 리팩토링한 모습입니다. 아래와 같이 MutableStateFlow의 update() 함수를 사용하면 여러 스레드에서 동시에 상태값을 수정하는 상황에서도 안전하게 상태값을 수정할 수 있으며, 크리티컬 섹션 문제도 해결할 수 있습니다.
viewModelScope.launch(Dispatchers.IO) {
_uiState.update { state ->
state.copy(
text = "my name"
)
}
}
viewModelScope.launch(Dispatchers.Default) {
_uiState.update { state ->
state.copy(
checked = state.checked.not()
)
}
}
결론
안드로이드에서 StateFlow를 사용할 때 동시 작업으로 값을 업데이트하는 경우, 원자성이 보장되지 않는 문제와 이를 해결하기 위한 방법에 대해 알아보았습니다. MutableStateFlow의 update() 함수가 생기기 전에는 개발자가 mutex를 생성해서 관리해주어야 했지만, 코틀린 코루틴 1.5.1 이상 버전에서는 위의 예제처럼 update() 함수를 통해 편하고 안전하게 상태값을 업데이트할 수 있습니다.
참고자료
https://sonseungha.tistory.com/642
[Android] MutableFlowState의 원자성 보장
정리하기 앞서 안드로이드 클린 아키텍처에 관해 공부 및 코드 분석을 진행하던 중 궁금한 부분이 생겼다. UI와 ViewModel의 상태를 data class와 MutableStateFlow를 겹합하여 상태를 관리하는 구조를 확
sonseungha.tistory.com
[안드로이드 Coroutine Flow] State Flow 원자성 보장
안드로이드의 State Flow는 Kotlin Coroutine의 일부로, 상태 관리를 위해 만들어진 Flow입니다. 상태가 없는 Flow에 상태를 보유할 수 있는 기능을 추가해서 개발자가 상태를 보다 쉽고 편리하게 관리할
dev-inventory.com
https://choi-dev.tistory.com/179
StateFlow 원시성 보장하기
StateFlow를 MVVM 패턴에서 UI 상태를 관리하고 반영하는데 사용하곤 했다. flow를 반복적을 관찰하며 최신 UI 상태를 얻어서 view에 보여줄 수 있는데 class MainViewModel: ViewModel(){ _viewState.value = _viewState.va
choi-dev.tistory.com
https://velog.io/@sdhong0609/MutableStateFlow-setValue-vs-update
MutableStateFlow setValue vs update
안드로이드에서 흔히 MutableStateFlow의 상태값을 변경할 때 setValue 혹은 update 함수를 통해 상태값을 변경한다. 그런데 언제 setValue를 사용해야 하고 언제 update 함수를 사용해야 할까? 1. 동시성 문제
velog.io
https://daldalhanstory.tistory.com/327?category=1094800
Android/Kotlin Flow를 이용하여 데이터 상태에 따라 버튼 활성화 비활성화 변경하기 TIL # 86
상황은 이렇습니다. 다음과 같이 3개의 EditText가 있고, 이 EditText의 글씨가 모두 써졌으면, 아래 버튼이 활성화되게끔 하려고 합니다. 우선 저는 상태에 따른 모델 값이 필요하다고 생각하여 다음
daldalhanstory.tistory.com