asStateFlow()/asSharedFlow()는 왜 써야할까?
최근 LiveData를 대체하여 StateFlow/SharedFlow를 사용한 예제들이 점점 많아지고 있다. 수많은 예제를 보다 보면 하나 눈에 걸리는게 있을 것이다. 적어도 나는 그랬다. LiveData에서는 외부에서 ViewModel 내 변수의 값을 수정할 수 없게 하기 위해 아래와 같이 코드를 작성해왔다.
private val _liveStr: MutableLiveData<String> = MutableLiveData()
val liveStr: LiveData<String> = _liveStr
하지만 StateFlow/SharedFlow는 같은 목적을 위해 아래와 같이 작성한다.
private val _flowText: MutableStateFlow<String> = MutableStateFlow("Hello")
val flowText: StateFlow<String> = flowText.asStateFlow()
왜 asStateFlow()/asSharedFlow()
라는 메소드를 사용할까?
asStateFlow()/asSharedFlow()
StateFlow/SharedFlow 뿐만 아니라 LiveData에서도 Mutable 객체는 내부에서만 사용하고 외부에서는 Immutable 객체를 노출시키는게 좋다. 왜 그래야 할까? 총 세 가지 이유가 있다.
- 불변성 보장: 외부에서 값을 수정할 수 없도록 방지한다.
- 코드의 안전성: 내부 상태를 안전하게 관리하며, 의도하지 않은 데이터 변경을 방지한다.
- 캡슐화: 데이터 흐름의 제어권을 ViewModel 내부로 제한한다.
그러면 이렇게 말할 수 있다. 쓰는건 좋은데 굳이 asStateFlow()/asSharedFlow()
를 써야 할까?
물론 안 써도 의도대로 동작은 될 것이다. 하지만 공식 문서에서는 asStateFlow()/asSharedFlow()
를 쓰는 것을 권장한다.
이하 내용은 StateFlow와 SharedFlow의 내용이 동일하므로 StateFlow만 가지고 설명하겠다.
// asStateFlow() 사용 예시
private val _firstFlowText: MutableStateFlow<String> = MutableStateFlow("Hello")
val firstFlowText: StateFlow<String> = _firstFlowText.asStateFlow()
// asStateFlow() 미사용 예시
private val _secondFlowText: MutableStateFlow<String> = MutableStateFlow("Hello")
val secondFlowText: StateFlow<String> = _secondFlowText
위처럼 하나는 asStateFlow()
를 사용하여 StateFlow를 선언했고 하나는 그대로 넣어 StateFlow를 선언했다. Activity에서 값을 출력해보면 아래와 같이 동일한 결과가 나온다.
// MainActivity.kt
println(viewModel.firstFlowText.collectAsState().value) // Hello
println(viewModel.secondFlowText.collectAsState().value) // Hello
하지만, 변수 타입을 출력하면 이 둘이 명백히 다르다는 것을 알 수 있다.
// MainActivity.kt
println(viewModel.firstFlowText::class.simpleName) // ReadOnlyStateFlow
println(viewModel.secondFlowText::class.simpleName} // StateFlowImpl
그 이유는 asStateFlow()
확장 함수의 내부 구조를 보면 이해할 수 있다.
/**
* Represents this mutable state flow as a read-only state flow.
*/
public fun <T> MutableStateFlow<T>.asStateFlow(): StateFlow<T> =
ReadonlyStateFlow(this, null)
asStateFlow()
확장 함수는 ReadonlyStateFlow
라는 새로운 객체를 생성하여 반환한다. ReadonlyStateFlow
는 MutableStateFlow
와 관련이 없는 클래스이다.
private class ReadonlyStateFlow<T>(
flow: StateFlow<T>,
@Suppress("unused")
private val job: Job? // keeps a strong reference to the job (if present)
) : StateFlow<T> by flow, CancellableFlow<T>, FusibleFlow<T> {
override fun fuse(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow) =
fuseStateFlow(context, capacity, onBufferOverflow)
}
StateFlowImpl
은 상태 흐름을 구현하는 실제 클래스이다. 이는 MutableStateFlow
와 관련되어 있는 클래스이다.
private class StateFlowImpl<T>(
initialState: Any // T | NULL
) : AbstractSharedFlow<StateFlowSlot>(), MutableStateFlow<T>, CancellableFlow<T>, FusibleFlow<T> {
private val _state = atomic(initialState) // T | NULL
private var sequence = 0 // serializes updates, value update is in process when sequence is odd
...
}
여기서 왜 asStateFlow()
를 써야 하는지에 대한 이유를 알 수 있다. 두 변수 모두 Activity에서 직접 접근하여 상태 변경이 가능한지 테스트해봤다.
// MainActivity.kt
// 1
(viewModel.firstFlowText as MutableStateFlow<String>).value = "changed"
/*
java.lang.ClassCastException: kotlinx.coroutines.flow.ReadonlyStateFlow cannot be cast to kotlinx.coroutines.flow.MutableStateFlow
*/
// 2
(viewModel.secondFlowText as MutableStateFlow<String>).value = "changed" // 변경됨
asStateFlow()
를 사용한 firstFlowText
는 캐스팅 과정에서 ClassCastException
이 발생했다. ReadonlyStateFlow
클래스는 위에서 말했듯 MutableStateFlow
클래스와 전혀 상관없는 클래스이다. 그렇기 때문에 수정이 불가능했다. 하지만 StateFlowImpl
클래스는 MutableStateFlow
클래스와 관련되어 있기 때문에 캐스팅하여 수정이 가능했다. 확실하게 안정성을 보장하는 코드를 작성하기 위해서는 asStateFlow()
확장함수를 사용하는 것이 좋다는 것을 알 수 있다.