[Coroutines] Flow(2) - SharedFlow vs StateFlow, SharedFlow로 해결한 스타카토의 재시도 문제

hxeyexn·2025년 6월 22일

Kotlin Coroutines

목록 보기
2/5

목차

  • Intro
  • LiveData? Flow?
  • SharedFlow vs StateFlow
    • SharedFlow
    • StateFlow
  • Outro

선행지식

  • Kotlin Coroutines
  • Flow
  • Hot Flow vs Cold Flow
  • MVVM

Intro

지난 포스팅에서는 Flow에 대해서 알아보았다. 이번에는 안드로이드 개발에서 상태 관리를 위해 자주 쓰이는 SharedFlow와 StateFlow에 대해 더 자세히 알아보겠다. 특히 SharedFlow는 스타카토에서 발생한 문제를 해결하는 데 활용한 경험이 있어, 그 문제를 중심으로 이야기해 보려 한다.



LiveData? Flow?

LiveData는 데이터 변경을 쉽게 감지할 수 있는 데이터 홀더 클래스이다. 변경을 감지하면 데이터를 자동으로 업데이트해 준다. 또한 생명 주기(LifeCycle)를 인식하여 활성 상태에서만 업데이트를 전달하기 때문에 구성 변경 시 메모리 누수를 방지할 수 있다. 안드로이드 앱 개발자에 굉장히 친숙하고, 편리한 도구이다.

간혹 StateFlow의 등장으로 LiveData는 이제 사라져야 한다고 말하는 개발자들이 있다. 글쎄🤔, 나는 잘 모르겠다. Flow는 기본적으로 생명 주기를 인식하지 않는다. 개발자가 수동으로 구독을 해지하지 않으면 화면이 비활성 상태일 때도 데이터를 계속 수집하게 된다. 이는 불필요한 연산이나 메모리 누수로 이어질 수 있다. 즉, LifeCycle과 밀접하게 연관된 작업이라면 LiveData가 더 적합한 선택이 될 수도 있다.

따라서 무작정 StateFlow와 SharedFlow를 사용하는 것이 반드시 좋은 선택은 아니다. 마치 팀에 맞는 컨벤션을 수립하듯, Flow 또한 자신만의 명확한 기준과 목적을 가지고 사용하는 것이 더 중요하다.

자신만의 명확한 기준과 목적이 있거나 구독 관리를 철저히 한다면, 혹은 학습을 위한 사용이라면 충분히 의미 있다고 생각한다.



SharedFlow vs StateFlow

SharedFlowStateFlow는 Kotlin의 Flow를 기반으로 만들어졌기 때문에 안드로이드에 종속되지 않으며, 상태 변화를 효율적으로 전달하고, 여러 소비자에게 값을 전달할 수 있다.

SharedFlow

스타카토는 학습을 위한 목적으로 Flow를 사용하고 있으며, SharedFlow의 특성을 활용해 문제를 해결한 경험이 있다. 문제를 살펴보며, SharedFlow는 어떤 특성이 있는지 알아보자.

상황
홈 화면에는 지도를 보여주는 프래그먼트와 카테고리 목록을 보여주는 프래그먼트가 각각 자리 잡고 있다. 이때 네트워크 오류가 발생하면, 재시도를 유도하기 위해 스낵바를 노출한다. 사용자가 스낵바의 ‘재시도’ 버튼을 누르면, 지도 화면과 카테고리 목록은 각자의 ViewModel에 정보를 다시 요청해야 한다.

네트워크 연결 불안정네트워크 연결 안정 & 재시도 완료

문제
1. SingleLiveData 사용

SingleLiveData는 특성상 지도 화면과 카테고리 목록 화면 중 오직 하나만 이벤트를 소비할 수 있다. 이에 따라 카테고리 목록은 정상적으로 다시 조회되었지만, 지도에 표시할 마커 목록은 재로딩되지 않았다.

2. LiveData 사용
LiveData를 사용했을 때는 지도 화면과 카테고리 목록 화면 모두 이벤트를 소비해, 각 화면이 정상적으로 재로딩되었다. 하지만 LiveData는 이벤트를 한 번만 소비하지 않기 때문에 다른 화면으로 이동한 뒤 홈 화면으로 돌아올 때마다 API가 불필요하게 다시 호출되는 현상이 나타났다.

해결 - SharedFlow 사용
SharedFlow를 사용하니 지도 화면과 카테고리 목록 화면 모두 정상적으로 재로딩되었고, 다른 화면으로 이동한 뒤 돌아올 때마다 API가 불필요하게 다시 호출되는 현상도 사라졌다.

SingleLiveData와 LiveData로는 해결되지 않던 문제가 SharedFlow를 활용해 해결되었다. SharedFlow의 어떤 특성 때문에 이 문제를 해결할 수 있었을까?


Kotlin Docs | SharedFlow

hot Flow that shares emitted values among all its collectors in a broadcast fashion, so that all collectors get all emitted values. …

SharedFlow는 데이터를 보내면 모든 수집기에서 전송된 값을 수신하는 Hot Flow이다.

또한, replay 값을 설정할 수 있으며 기본값은 0이다. replay는 새로운 구독자에게 과거의 값을 얼마나 전달할지를 지정할 수 있다. 만약 replay = 4 로 설정하면, 가장 최근에 발행된 값 4개를 다시 전달할 수 있다. 따라서 replay = 0 으로 설정하면 이전값을 전달하지 않는다.

public interface SharedFlow<out T> : Flow<T> {
    public val replayCache: List<T>

    override suspend fun collect(collector: FlowCollector<T>): Nothing
}

@Suppress("FunctionName", "UNCHECKED_CAST")
public fun <T> MutableSharedFlow(
    replay: Int = 0,
    extraBufferCapacity: Int = 0,
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
): MutableSharedFlow<T> {
    ...
    return SharedFlowImpl(replay, bufferCapacity, onBufferOverflow)
}

이러한 특성은 다음과 같은 문제를 해결하는 데 적합했다.

  • 특성 1. 데이터를 보내면 모든 수집기에서 전송된 값을 수신 → 지도 화면과 카테고리 목록 화면 모두 동시에 이벤트를 수신하고 재로딩할 수 있다.
  • 특성 2. replay 설정 가능replay = 0 으로 설정하면, 화면을 벗어났다가 다시 돌아와도 이전 이벤트가 재전달되지 않는다. 따라서 재시도 이벤트가 한 번만 소비되도록 보장할 수 있다.

이처럼 SharedFlow는 여러 수집자에게 동시에 값을 전달하면서, 이벤트를 한 번만 소비하도록 제어할 수 있기 때문에 LiveData나 SingleLiveData로는 해결하기 어려웠던 문제를 해결할 수 있었다. 단, SharedFlow는 생명 주기를 인식하지 않기 때문에 이와 관련해서는 개발자가 수동으로 처리해 줘야 한다.

Activity 처리 방법

class MyActivity : AppCompatActivity() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        lifecycleScope.launch {
            // STARTED일 때 실행해 수집, STOPPED 상태가 되면 내부적으로 코루틴이 취소되면서 수집 중단
            // Runs the block of code in a coroutine when the lifecycle is at least STARTED.
            // The coroutine will be cancelled when the ON_STOP event happens and will
            // restart executing if the lifecycle receives the ON_START event again.
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect { ... }
            }
        }
        ...
    }
}

Fragment 처리 방법

class MyFragment : Fragment() {
    ...
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        ...
        viewLifecycleOwner.lifecycleScope.launch {
            // repeatOnLifecycle launches the block in a new coroutine every time the
            // lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                ...
                viewModel.someDataFlow.collect {
                    ...
                }
            }
        }
    }
}

StateFlow

Kotlin Docs | StateFlow

SharedFlow that represents a read-only state with a single updatable data value that emits updates to the value to its collectors. A state flow is a hot flow because its active instance exists independently of the presence of collectors. Its current value can be retrieved via the value property.

State flow never completes. …

StateFlow는 SharedFlow의 개념을 확장한 것이며, replay 인자 값이 1인 SharedFlow와 비슷하게 작동한다. StateFlow는 수집기의 존재 여부와 관계없이 활성 인스턴스가 독립적으로 존재하기 때문에 Hot Flow이다.

public interface StateFlow<out T> : SharedFlow<T> {
    public val value: T
}

public interface MutableStateFlow<T> : StateFlow<T>, MutableSharedFlow<T> {
    public override var value: T

    public fun compareAndSet(expect: T, update: T): Boolean
}

StateFlow는 상태를 관리하고 관찰한다는 점에서 LiveData와 유사하다. 하지만 큰 차이점이 있다. StateFlow는 절대 완료되지 않으며, 무엇보다 LiveData와 달리 생명 주기를 인식하지 않는다.

LiveData.observe() 는 뷰가 STOPPED 상태로 전환되면 자동으로 관찰자가 해제된다. 반면, StateFlow는 생명 주기를 인식하지 않기 때문에 수집이 자동으로 중단되지 않는다. LiveData와 동일한 방식으로 동작하게 하려면, SharedFlow와 동일하게 Lifecycle.repeatOnLifecycle 블록 안에서 Flow를 수집해야 한다.



Outro

개인적인 생각으로는, LifeCycle과 밀접하게 연관된 작업이라면 LiveData를 사용하는 것이 더 나은 선택일 수 있다. 특히, 수동으로 구독을 해지할 자신이 없다면 SharedFlow와 StateFlow는 오히려 위험할 수 있다.

유행에 따라 SharedFlow나 StateFlow를 무작정 사용하는 것보다는, 상황에 맞는 적절한 선택 기준을 가지고 사용하자.



참고자료

Kotlin Docs | SharedFlow
Kotlin Docs | StateFlow
코틀린 코루틴

profile
Android Developer

0개의 댓글