StateFlow 와 SharedFlow
코루틴 공식 가이드 읽기 Part 9 — Dive1
Korean [English]
(목차로 돌아가기)
우리는 애플리케이션을 개발할 때 필연적으로 “상태”와 “이벤트” 를 다루게 됩니다.
상태(State)
UI 애플리케이션에서 다루는 상태에 대해 생각 해봅시다.
우선 사용자의 인증 상태를 생각해 볼 수 있습니다. 사용자의 인증 상태는 크게 “로그아웃”, “로그인”, “계정 잠김” 등으로 정의 될 수 있으며 반드시 한가지 상태를 갖고 있습니다.
또 한가지는 화면에 표현하기 위한 데이터의 상태를 생각해 볼 수 있습니다.
이는 “로딩중”, “데이터 이용가능”, “오류” 등으로 정의 될 수 있으며 역시 반드시 한가지 상태를 갖고 있습니다.
애플리케이션은 필요한 상태 머신을 정의하고, UI 변경이나 비지니스 로직의 수행 등에 따라 상태를 전이해 가며 실행됩니다.
MVVM 아키텍쳐를 이용할 경우 보통 UI 와 관련된 “상태” 들은 ViewModel (VM) 에 위치하여 ViewModel 에 바인드 된 View 들이 상태를 표현할 수 있도록 합니다.
Kotlin 을 사용하여 이런 상태들을 정의 할 때 상태가 상태 자체로 의미가 있고 부가적인 데이터를 갖고 있지 않다면 Enum 을 이용하여 정의하기도 하고, 부가적인 데이터가 필요하다면 Sealed class 를 이용하여 정의하기도 합니다.
Android 에서는 ViewModel 에서 이런 상태 데이터를 다루기 위한 장치로 LiveData 를 제공하고 있습니다. LiveData 는 Android lifecycle 과 자연스럽게 연동되므로 편리하게 사용할 수 있습니다. 다만, LiveData 는 Lifecycle 이 있는 UI 와 인터렉션 하도록 디자인 되었으며 비지니스 레이어에서의 사용에는 무리가 있습니다. 특히, 비지니스 레이어를 플랫폼 독립적으로 가져가고자 한다면 더욱이 그렇습니다.
이 경우 과거에는 Reactive streams, 특히 Android 에서는 RxJava/Kotlin 구현을 사용하였습니다. RxJava 에서도 Subject 를 이용하여 이를 구현하였는데 다양한 타입의 Subject 가 있지만 기본 상태를 갖고 있으며 모든 구독자에게 최신 상태를 전달해주는 BehaviorSubject 가 “상태” 관리에 주로 사용 되었습니다.
이제 많은 Android Application 들이 Kotlin 으로 작성되고 있으므로 RxJava/Kotlin 의존성을 가져 오지 않더라도 경량화된 Reactive streams 구현체 라고 볼 수 있는 Flow 를 이용하여 이를 구현할 수 있습니다. Flow 는 가이드에서 이야기한 것처럼 여타 Observable (RX) 과 같이 기본적으로 콜드 스트림 입니다. 즉 구독 시점에 스트림이 생성되어 데이터를 처음부터 수신합니다. 이를 RX 의 Subject 처럼 Hot Observable 로 제공해 주는 Flow 구현으로 StateFlow
, SharedFlow
가 있습니다. 그 중에서 기본값을 가지고 모든 구독자에게 최신 상태를 전달해 주는 것은 StateFlow 입니다.
Android ViewModel 에서 Flow 의 사용 역시 AndroidX 를 통해 제공되는 Coroutine viewModelScope 를 이용하여 ViewModel lifecycle 에 바인딩 되어 편리하게 사용할 수 있습니다. 더욱이 많은 AndroidX 및 서드파티 라이브러리들이 중단 함수를 제공하고 있기 때문에 Flow 는 더욱 빛을 발할 수 있습니다. Flow 가 Rx Observable 과 비교하여 갖는 강력한 장점 중 하나는 FLow chain 에서 중단함수를 사용할 수 있다는 것 입니다. 이것은 코드의 많은 부분에서 가독성을 높이고 유지보수를 용이하게 해줄 수 있습니다.
StateFlow 를 사용하는 코드를 잠시 살펴볼까요?
refreshDataUseCase 는 비지니스 레이어에서 데이터를 동기화하고 최신 데이터를 리스트 형태로 가져온다고 생각합시다.
먼저 6~8 번 라인에서 StateFlow 를 정의하고 있습니다. _dataState
는 ViewModel 내부에서 사용하기 위한 변경 가능한(Mutable) Flow 이고 ViewModel 외부로 노출하기 위한 dataState
는 위에서 생성한 Flow 를 asStateFlow() 함수를 이용하여 Immutable Flow 로 변환한 Flow 입니다.
11 번 라인의 refreshData() 가 호출되면 ViewModel 스코프에서 코루틴이 생성되어 UseCase 의 중단함수를 호출하게 되고, 결과가 도착하면 성공/실패 에 따라 적당한 데이터로 변환하여 Flow 로 전달하게 되고, 이것은 dataState
를 구독하고 있는 UI 를 갱신하게 만듭니다.
지금까지 StateFlow 에 대해서 간략하게 살펴보았습니다.
SharedFlow 는 무엇일까요?
이벤트(Event)
앞서 살펴본 것처럼 우리는 애플리케이션 개발을 할 때 필요한 상태 머신을 정의하고, 애플리케이션은 정의 된 상태들로 전환해 가면서 실행됩니다. 이러한 애플리케이션 “상태” 와 함께 우리가 애플리케이션을 개발할 때 고려해야 하는 다른 한가지는 바로 “이벤트” 입니다.
상태 머신은 늘 기본 상태를 갖고 동작하지만 이벤트는 기본값 없이 특정 상황이 발생했을 때 구독자들에게 발생한 상황을 이벤트라는 형태로 전달합니다. 그러면 구독자의 입장에서 상태와 이벤트는 어떻게 다를까요?
- 상태는 기본값이 있지만, 이벤트는 기본값이 없습니다.
- 상태는 신규 구독 시 가장 최근 값을 받지만, 이벤트는 구독 이후 발생한 값을 받습니다.
(위 내용은 기본적으로 UI 애플리케이션에서 상태와 이벤트에 대한 일반적인 내용이며 각 프레임워크에서 제공하는 상태나 이벤트 매커니즘의 옵션 설정을 통해 동작은 변경될 수 있습니다.)
이러한 이벤트는 어떤 경우에 사용될까요?
일반적으로 UI Layer 에서 사용자와 뷰의 인터렉션으로 인해 발생한 이벤트를 정의하여 전달하거나, 시스템에 발생 한 메모리 부족, 인증 오류 발생 등의 이벤트에 관련 컴포넌트들이 대응할 수 있도록 하기 위해 사용합니다.
SharedFlow 를 사용하는 코드를 잠시 살펴봅시다.
전체적인 구조는 StateFlow 와 유사합니다. 다만 MutableSharedFlow 생성 시 몇 가지 옵션을 전달하여 SharedFlow 의 동작을 재정의 하고 있습니다.
- replay = 0 : 새로운 구독자에게 이전 이벤트를 전달하지 않음
- extraBufferCapacity = 1 : 추가 버퍼를 생성하여 emit 한 데이터가 버퍼에 유지되도록 함
- onBufferOverflow = BufferOverflow.DROP_OLDEST : 버퍼가 가득찼을 시 오래된 데이터 제거
위 세가지 옵션을 통해 우리가 원하는 SharedFlow 를 생성할 수 있으며, 이는 우리가 RxJava/Kotlin 에서 사용하던 PublishSubject 와 유사하다고 할 수 있습니다.
지금까지 살펴본 Flow 들은 내부적으로 다음과 같은 계층 구조를 가지고 있습니다.
Flow <- SharedFlow <- StateFlow
SharedFlow 에서는 replayCache 와 그 크기를 정의하여 새로운 구독자에게 반복 전달할 값의 개수를 설정할 수 있도록 하며, 구독자들을 Slot 이라는 형태로 관리하여 값이 전달 될 시 액티브한 모든 구독자에게 새로운 값이 전달 되도록 합니다.
SharedFlow 를 상속하는 StateFlow 는 추가로 기본값을 가지고 있으며, replaceCache 는 가장 최근 값 하나를 갖는 리스트를 replayCache 로 재정의합니다.
지금까지 간략하게 살펴본 Hot Flow 들을 애플리케이션 개발 시 상황에 따라 적절하게 사용한다면 더 간결하고 직관적인 코드를 작성할 수 있을 것이라 생각합니다.
끄읕!