Android 비동기 상태 관리: StateFlow & SharedFlow의 이해와 활용

Android 비동기 상태 관리: StateFlow & SharedFlow의 이해와 활용
Photo by Daniel Romero / Unsplash

이제는 Android UI 개발 과정에서 XML 대신 Jetpack Compose를 사용하는 곳이 더 많아지고 있다. 선언적인 접근 방식을 택한 Jetpack Compose는 직관적이고 재사용성이 좋다는 이유 등으로 사람들에게 인기를 끌고 있다. 이 과정에서 다양한 라이브러리들이 새롭게 사용되고 있는데 그 중 기존 MVVM, MVI 패턴에서 많이 사용되었던 LiveData가 StateFlow/SharedFlow로 일부 대체되고 있어, 해당 내용을 다뤄보려고 한다.

먼저, Flow에 대해서 알고 가야 한다.

Flow

Flow는 Kotlin Coroutines의 일부분으로, 비동기 데이터 스트림을 처리하는 API이다. Flow는 소비자가 구독을 시작해야만 데이터 생산이 시작되며, 이는 메모리 효율성을 높여준다는 장점이 있다. 또한, 데이터를 비동기적으로 가져와 UI를 차단하지 않고 효율적으로 업데이트할 수 있게 해준다. 결과적으로 Flow는 시간에 따라 발생하는 데이터 변화를 처리하는데 유용하며, Android에서는 StateFlow, SharedFlow의 형태로 구현하여 상태 관리와 이벤트 처리를 한다.

Android 공식 문서에는 "Flow"를 "흐름"이라고 번역해놨다.. 웬만한 공식 문서는 꼭 영어로 보는 것을 추천한다.

StateFlow/SharedFlow

StateFlow와 SharedFlow는 모두 Kotlin Coroutines의 일부로, 특히 Jetpack의 코루틴을 사용할 때 상태 관리를 효과적으로 도와주는 Flow 유형이다.

StateFlow

StateFlow는 상태(State)를 관리하는데 특화된 Flow이다. Android에서는 주로 UI 상태를 표현하는데 많이 사용된다. 항상 최신 값을 유지하며, 수신자는 언제든지 현재 상태를 가져올 수 있다. 데이터가 변하지 않더라도 수신자가 구독하게 되면 값이 변경될 때마다 즉시 알림을 받아볼 수 있다. 또한, StateFlow는 초기값을 필요로 한다.

SharedFlow

SharedFlow는 상태를 가지지 않으며, Stateless 방식으로 동작한다. 또한, 여러 수신자와 데이터를 공유하는데(share) 적합하여 이벤트를 방송하는 방식으로 사용된다. SharedFlow 또한 이벤트가 발생할 때마다 모든 수신자에게 전달하며, 각 수신자는 기존 이벤트에 접근할 필요가 없을 때 적합하다. 구독 관련하여 구독 취소 및 버퍼링 등 다양한 제어 방법을 제공하여, 복잡한 동작을 필요로 하는 경우 유용하다.

그러면 왜 StateFlow/SharedFlow를 LiveData 대신 사용하라고 권장하는 것일까? 이를 알기 위해서는 이 둘이 무엇이 다른지 차이점에 대해 이해해야 한다.

LiveData vs StateFlow/SharedFlow

  1. 동작 방식의 차이LiveData는 Activity, Fragment, Service와 같은 Android 컴포넌트의 생명주기(LifeCycle)를 인식하여, UI가 활성화되었을 때만 데이터를 업데이트한다. (Lifecycle-aware) 이는 메모리 누수를 방지하며, 메모리 안전성을 높여준다. 그리하여 LiveData는 주로 UI 컴포넌트에 대하여 데이터 변경을 할 때 사용한다.StateFlow/SharedFlow는 Kotlin의 Flow API를 기반으로 사용하여, Lifecycle과 독립적으로 작동한다. 즉, Livecycler을 관리하지 않는 다른 Coroutine에서도 사용할 수 있다. 또한, 비동기 흐름을 처리하기 위해 Coroutine을 활용하므로, 복잡한 비동기 작업에 더 유리하다.
  2. 데이터 업데이트 vs 구독LiveData는 주로 Observer에게 변화된 데이터를 통지한다. Observer는 LiveData의 상태를 구독하고, 데이터가 변경되면 UI를 업데이트 한다.StateFlow/SharedFlow는 Flow API를 사용하여 데이터의 흐름을 처리한다. 여러 곳에서 동일한 데이터를 구독할 수 있어, 데이터 흐름 관리에 유연성을 제공한다.
  3. LiveData의 한계LiveData는 기본적으로 값의 변화에 대한 구독에 중점을 두기 때문에, 단발성 이벤트를 처리하는데 어려움이 있다. 예를 들어, 버튼 클릭과 같은 이벤트가 발생했을 때 이를 처리하기가 복잡할 수 있다. 또한, LiveData와 비동기 작업을 통합하는 것이 상대적으로 어려워, 비동기 작업이 많은 경우 StateFlow를 사용하는 것이 더 효과적이다.

StateFlow, SharedFlow 사용 예시

StateFlow 예시

StateFlow는 주로 화면 상태 관리에 사용한다. 예를 들면 쇼핑 앱에서 '장바구니', '상품 상세 보기', '결제' 같은 서로 다른 상태를 관리하기 위해 사용할 수 있다.

// Example (StateFlow, MVI)
class CartViewModel : ViewModel() {
  
    private val _viewState = MutableStateFlow<CartViewState>(CartViewState.Loading)
    val viewState: StateFlow<CartViewState> = _viewState.asStateFlow()
    
    fun loadCart() {
        // 장바구니 로드 로직
        _viewState.value = CartViewState.Loaded(cartItems)
    }
}

SharedFlow 예시

SharedFlow는 사용자에게 새로운 알림이나 이벤트가 발생했을 때, 이를 구독 설정에 따라 전달하기 위해 사용할 수 있다. 예를 들어, 채팅 앱에서 새로운 메시지가 도착했을 때 UI에 이를 표시할 수 있다.

// Example (SharedFlow)
class ChatViewModel : ViewModel() {
  
    private val _newMessageFlow = MutableSharedFlow<Message>()
    val newMessageFlow: SharedFlow<Message> = _newMessageFlow.asSharedFlow()
    
    suspend fun sendMessage(message: Message) {
        // 메시지 전달
        _newMessageFlow.emit(message)
    }
}

LiveData, Flow 동시에 사용하기

그러면 LiveData는 아예 쓰지 말고 StateFlow/SharedFlow로 넘어가는게 좋은거 아니야? 라고 생각할 수 있겠지만 각각의 장단점이 있기 때문에 특정 상황에서는 두 가지를 함께 사용하는 것이 유용할 수 있다.

혼용할 때의 장점은 다음과 같다.

  1. 기존 코드와 통합새로운 코드를 작성하는게 아니라면 기존의 코드들은 웬만해서 LiveData를 사용하고 있었을 것이다. 그렇다면 새로운 구조나 기능에 StateFlow를 도입할 때 모두 바꿀 필요 없이 두 가지를 혼합하여 사용할 수 있다. 이를 통해 점진적으로 마이그레이션 할 수 있는 유연성을 제공한다.
  2. UI 생명 주기LiveData는 Android의 생명 주기를 자동으로 관리하기 때문에 Activity나 Fragment와 함께 사용할 때 매우 유용하다. 이와 함께 StateFlow를 사용하여 컴포넌트 간 상태 공유를 용이하게 할 수 있다.
  3. 비동기 데이터 처리StateFlow는 Kotlin Coroutine과 함께 비동기 방식으로 데이터를 처리할 수 있는 강력한 기능을 제공한다. 이는 네트워크 통신 및 복잡한 데이터 변환 작업과 잘 어울린다. 기존 LiveData에서 비동기 작업에는 StateFlow를 사용하는 방식으로 혼용할 수 있다.

예시

class MyViewModel : ViewModel() {
  
    private val _stateFlow = MutableStateFlow<MyState>(initialState)
    val stateFlow: StateFlow<MyState> get() = _stateFlow
    private val _liveData = MutableLiveData<MyState>()
    val liveData: LiveData<MyState> get() = _liveData
  
    init {
        // StateFlow를 통해 데이터 업데이트
        viewModelScope.launch {
            _stateFlow.collect {
                _liveData.value = it // LiveData로 업데이트
            }
        }
    }
    fun updateState(newState: MyState) {
        _stateFlow.value = newState // 상태 업데이트
    }
}
Park Sang-uk

Park Sang-uk

South Korea