StateFlow vs SharedFlow

염준우·2022년 10월 28일
0
post-thumbnail

어느 한 곳에 면접을 보게 되었다.
과제를 완료해서 과제에 대한 이야기를 하는 도중에 "왜 StateFlow를 사용하셨나요??" 라는 말을 듣게 되었다.
난 여기서 StateFlow를 통해서 collect를 받아오면서 사용할 수 있기 위해서 사용했다라고 답했다.
여기서 이제 내가 실수했던 문제가 나오게 됐는데, SharedFlow 와 StateFlow 의 차이를 물으셨다.
난 여기서 StateFlow는 콜드 흐름이고, SharedFlow 는 핫 흐름이라고 답했다. (그냥 그랬다구요.)
그러자 면접관님께서는 StateFlow도 핫 흐름이다. 들어가서 확인해보면 SharedFlow를 상속받고 있다.라고 하셨다.
나는 여기서 내가 내가 잘못 알고 있구나라고 느꼈고, 어떤게 다른지 궁금해졌다.

그래서 찾아봤을 때 가장 큰 건 두개가 있는것 같다.

Value 의 존재

한번 StateFlow를 들어가봤다.

public interface StateFlow<out T> : SharedFlow<T> {
    /**
     * The current value of this state flow.
     */
    public val value: T
}

이렇게 되어있다. SharedFlow의 값을 그대로 상속받는데, 여기서 value 가 더해지는 거였다.
SharedFlow 에서는 value가 없고, StateFlow 만 가지고 있는 이유가 이거였다고 생각이 든다.
근데 StateFlow에서 value는 어떻게 동작하는지 알고싶었다.

// StateFlow,kt ( StateFlowImpl )
 @Suppress("UNCHECKED_CAST")
    public override var value: T
        get() = NULL.unbox(_state.value)
        set(value) { updateState(null, value ?: NULL) }

여기서 가지고 올때 NULL.unbox 라는 함수를 통하여 현재 _state를 통해 관리가 되고 있다. ( _state는 atomic 으로 set() 내의 updateState()에서 계속 변화되고 있다.
이렇게 때문에, StateFlow에서는 우리가 원하는 타입으로 value 를 가지고 올 수 있는 것이다.

Equality Check

아마 이게 가장 중요한 부분이 아닐까 싶다.
SharedFlow 는 이전의 값과 현재의 값이 같아도 collect 하는 과정에서 정상적으로 collect 가 진행되는 느낌인데, StateFlow 는 그런 느낌을 받지 못했다.
그래서 한번 찾아보려고 한다.

// StateFlow.kt
// Conflate value emissions using equality
	if (oldState == null || oldState != newState) {
		collector.emit(NULL.unbox(newState))
		oldState = newState
	}

위 코드는 StateFlowImpl에 collect함수에 들어있는 코드이다. oldState가 null이거나, oldState랑 newState의 값이 다를 때만 emit 을 진행하도록 설정되어 있다.
그렇다면 SharedFlow는 어떨까??

// SharedFlow.kt
 var newValue: Any?
	while (true) {
		newValue = tryTakeValue(slot) // attempt no-suspend fast path first
			if (newValue !== NO_VALUE) break
			awaitValue(slot) // await signal that the new value is available
		}
	collectorJob?.ensureActive()
	collector.emit(newValue as T)

위 코드는 SharedFlow 에 collect함수에 들어있는 코드이다. oldState 이런 것 없이 일단 emit하고 본다.
Equality Check 가 들어가지 않는다는 뜻이다.

안드로이드에서 사용방법

그래도 만들어서 우리 손으로 변경이 가능한 MutableSharedFlow 랑 MutableStateFlow로 비교를 해보도록 하겠다.

// SharedFlow.kt
public fun <T> MutableSharedFlow(
    replay: Int = 0,
    extraBufferCapacity: Int = 0,
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
): MutableSharedFlow<T> {
    require(replay >= 0) { "replay cannot be negative, but was $replay" }
    require(extraBufferCapacity >= 0) { "extraBufferCapacity cannot be negative, but was $extraBufferCapacity" }
    require(replay > 0 || extraBufferCapacity > 0 || onBufferOverflow == BufferOverflow.SUSPEND) {
        "replay or extraBufferCapacity must be positive with non-default onBufferOverflow strategy $onBufferOverflow"
    }
    val bufferCapacity0 = replay + extraBufferCapacity
    val bufferCapacity = if (bufferCapacity0 < 0) Int.MAX_VALUE else bufferCapacity0 // coerce to MAX_VALUE on overflow
    return SharedFlowImpl(replay, bufferCapacity, onBufferOverflow)
}

보통 우리가 MutableSharedFlow 를 만들때, 이 함수를 통해서 만들게 된다. StateFlow는 어떻게 만들까?

// StateFlow.kt
public fun <T> MutableStateFlow(value: T): MutableStateFlow<T> = StateFlowImpl(value ?: NULL)

굉장히 간결하다. 근데 여기서 한가지 의문점이 생겼다.
StateFlow도 SharedFlow인데, 만드는 방식에서 왜 SharedFlow 를 만들때 필요한 프로퍼티였던 replay, extrabufferCapacity, onBufferOverflow가 왜 없어졌을까?
먼저 buffer 관련해서는 tryEmit 쪽에서 이유를 찾았다.
SharedFlowImpl에서는 emit 을 할 때 replay, bufferCapacity, BufferOverflow등을 다 확인하면서 그에 맞게 변경이 진행되고 있는데,
SharedFlowImpl에서는 그냥 _state의 value만 바꿔주면 되니, 굳이 SharedFlow에서 받아올 필요가 없기 때문에 사용하지 않았다고 생각하면 될 것 같다.

결론

  • StateFlow는 SharedFlow 의 상속 클래스이므로, 핫 흐름이다.
  • StateFlow는 SharedFlow에서 value 와 Equality Check 가 들어간 클래스이다.(물론 더 있을 수 있다. 있으면 말씀 부탁드려요..)
  • 만들때 Default 값이 있어야 하고, Equality Check가 필요하고, value에 대한 접근이 필요할 때 StateFlow가 굉장히 좋은 수단이 될 수 있다.
    -~~ 면접을 볼 때는 안정을 취하고, 아는 척 하지 말자.~~
profile
항상 새로움을 추구하는 안드로이드 개발자입니다! 🏆

0개의 댓글