(Topic) 004. StateFlow vs SharedFlow

StateFlow vs SharedFlow

코루틴 플로우에 대해서 공부하다보면 Android 개발자라면 꽤 익숙한 이름들을 발견할 수 있다.

바로 StateFlowSharedFlow 이다.

이 두 가지는 모두 Hot Stream에 속하는 녀석이다.

각각 어떤 녀석인지 한 번 알아보자.

1. StateFlow

StateFlow는 문자 그대로 현재 상태를 표현하는 flow로 SharedFlow의 일종이다.

“상태”를 표현하기때문에 생성시 무조건 초기값이 필요하고, 하나의 업데이트 가능한 값을 가지는 게 특징이다.

상술했듯 StateFlow는 소비자의 존재와는 별개로 독립적으로 활성화된 객체를 가지고 있기 때문에 Hot Stream으로 분류된다.

매우 중요한 특징으로 StateFlow는 절대 완료되지않는다.

collect 함수를 호출하더라도 정상적으로 종료되지않고, launchIn 함수를 호출하여 시작된 코루틴도 종료되지않는다.

따라서 StateFlow에서 갑을 수집하는 녀석을 구독자(subscriber) 라고 부른다.

1.1 StateFlow의 생성과 상태 접근

StateFlowMutableStateFlow 클래스의 생성자로 만들 수 있다.

1
val stateFlow = MutableStateFlow(0)

상태의 접근은 기본 발행 함수인 emit을 쓸 수도 있지만 value 프로퍼티를 통해 접근할 수도 있다.

1
2
3
4
5
val stateFlow = MutableStateFlow(0)
var state = stateFlow.asStateFlow() // read-only
state = stateFlow
state.emit(1)
state.value = 1

모든 상태에 대한 변경은 모두 통합되므로 모든 구독자는 가장 최근값을 획득할 수 있다.

이처럼 상태 관리에 특화된 flow이기 때문에 모든 종류의 상태를 표현하는 data-model 클래스로 적합하며, combine등의 함수를 통해 여러 StateFlow의 값을 결합하는 등의 응용이 가능하다.

1.2. StateFlow 예제

아래 예제는 특정한 정수를 상태로 가지는 StateFlow를 캡슐화한 CouterModel 클래스이다.

이 클래스는 inc() 함수가 호출될 때마다 count 값을 1씩 증가시킨다.

1
2
3
4
5
6
7
8
class CounterModel {
private val _counter = MutableStateFlow(0) // private mutable state flow
val counter = _counter.asStateFlow() // publicly exposed as read-only state flow

fun inc() {
_counter.update { count -> count + 1 } // atomic, safe for concurrent use
}
}

1.3. 강력한 평등기반 결합(Strong equality-based conflation)

StateFlowvalueAny.equals 비교를 사용하여 결합되므로, 이전에 내보낸 값과 동일한 값을 소비자에게 송신하지않도록 처리된다.

바꿔말해 Any.equals의 규칙대로 작성되지않은 클래스에서는 StateFlow가 의도대로 동작하지 않을 수 있다.

1.4. StateFlow는 SharedFlow이다. (State flow is a shared flow)

StateFlow는 상술했듯 SharedFlow의 한 종류로, 상태를 공유하기 위한 특수 목적의 구현체이다.

따라서 SharedFlow의 모든 기본 규칙, 제약 조건, 연산자 등은 StateFlow로 그대로 계승된다.

다만 초기값을 가지는 것, 단 한 개의 최신 상태값을 가지는 것 그리고 SharedFlowresetReplayCache가 지원되지않는 것이 그 차이점이다.

SharedFlowStateFlow처럼 쓰려면 아래와 같이 작성하여 사용할 수 있다.

1
2
3
4
5
6
7
// MutableStateFlow(initialValue) is a shared flow with the following parameters:
val shared = MutableSharedFlow(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
shared.tryEmit(initialValue) // emit the initial value
val state = shared.distinctUntilChanged() // get StateFlow-like behavior

결론적으로 StateFlow는 객체의 최신 상태를 얻어오기위한 특수목적 이외에는 SharedFlow를 사용하는 것이 권장된다.

이 특수목적 이외에는 추가적인 버퍼링 작업, 초기값의 생략, 부가적인 값이 해당된다.

1.5. 동시성(Concurrency)

StateFlow의 모든 메서드는 모두 thread-safe 하며, 외부의 동기화없이도 복수개의 코루틴에서 안전하게 호출할 수 있다.

1.6. StateFlow를 구현하면서..

StateFlow는 메모리의 점유를 통한 소비와 자유로운 할당에 최적화되어있다.

상술했듯 thread-safe한 호출을 보장하기 위해 내부적으로 lock-unlock을 수행하지만 코루틴 내에서 별도의 dead-locks은 발생하지않도록 처리되어있다.

신규 구독자의 추가시 각각 O(1)의 시간 복잡도를 가지며, 상태값의 갱신의 경우 현재 활성화된 구독자의 수(=N) 에 비례한 O(N) 시간 복잡도를 가진다.