뷰모델을 다루다보면 계속 듣던 StateFlow SharedFlow에 대해 알아보려고한다.
그 동안 라이브 데이터를 사용해서 구현했는데
현업에선 StateFlow SharedFlow만 쓰고 라이브 데이터는 이제 잘 안쓴다는 얘기를 들었다.
이 둘의 라이브 데이터와 다른점은 코루틴에서 작동한다는점으로 알고 있는데
정확히 차이가 뭐고 라이브 데이터 대신 사용하는 이유가 뭘까?
StateFlow는 상태 관리에 최적화되어 있다.
StateFlow는 항상 초기 상태를 가지고, 구독자에게 최신 상태를 즉시 제공하는데 주요 특징은 상태의 현재 값에 항상 접근할 수 있다는점이다.
그래서 라이브 데이터와의 차이점이 뭔데?
라이브 데이터와의 가장 큰 차이점은 결국 코루틴이다
코루틴을 사용하기 때문에
높은 수준의 비동기 처리, 에러 핸들링, 그리고 데이터 스트림의 세밀한 제어가 가능해서
복잡한 데이터처리를 고효율로 가독성 좋게 처리할 수 있다.
라이브 데이터 같은 경우 간단한 데이터 바인딩이나 생명주기에 민감한 UI 업데이트가 필요한 경우에 사용한다고 한다.
Flow는 코루틴을 사용하기 때문에 백그라운드 에서도 데이터 스트림의 수집을 계속할수 있다.
라이브 데이터는 생명주기에 따라가기 때문에 메모리 누수에 대한 걱정이 없는데 Flow는 백그라운드에서 데이터 스트림을 수집할때 메모리 누수가 발생 할 수 있어서 코루틴 스코프를 관리해 줘야한다.
결국 Flow의 강점은 코루틴을 통한 비동기 처리와 에러 핸들링을 보다 세밀하게 제어하는데 있다.
StateFlow와 SharedFlow는 데이터 스트림을 더 세밀하게 제어할 수 있는 다양한 연산자를 제공하는데, 예를 들어, filter, map, debounce 등의 연산자를 이용해 데이터 스트림을 변환하고, 특정 조건에 따라 업데이트를 방출할 수 있어 복잡한 데이터 처리 로직을 구현할 때 매우 유용하다.
코루틴을 점점 사용하는 현업 특성상 Flow는 선호 될 수 밖에 없다.
여기 앱에 사용자가 검색어를 입력하고, 해당 검색어에 기반한 결과를 비동기적으로 가져와 표시하는 기능이 있다고 치자. 이때, 사용자가 입력하는 동안 너무 많은 네트워크 요청을 방지하고, 최신 검색어에 대한 결과만 표시하고 싶을때 어떻게 해야할까
class SearchViewModel : ViewModel() {
private val searchRepository = SearchRepository()
private val _searchQuery = MutableLiveData<String>()
private val handler = Handler(Looper.getMainLooper())
private var searchRunnable: Runnable? = null
// 검색 결과를 저장하는 LiveData
val searchResults = Transformations.switchMap(_searchQuery) { query ->
liveData {
if (query.length >= 3) {
val result = searchRepository.getSearchResults(query)
emit(result)
}
}
}
fun setSearchQuery(query: String) {
searchRunnable?.let { handler.removeCallbacks(it) }
searchRunnable = Runnable {
_searchQuery.value = query
}
// 사용자 입력 후 300ms 딜레이를 주는 디바운싱 로직
handler.postDelayed(searchRunnable!!, 300)
}
}
사용자가 값을 입력할때 마다 비동기 호출을 하면 굉장히 빠르게 변하는 데이터를 계속 서버에 보내서 호출을 하는것은 비효율 적이니, 라이브 데이터로 그부분을 해결하기 위해선 별도의 핸들러나 타이머 로직을 구현해서 수동으로 디바운싱 로직을 만들어야한다
그리고 Transformations.switchMap을 사용해서 LiveData의 각 값 변화에 대해 새로운 LiveData를 생성하고, 새로운 값이 들어올 때마다 이전 LiveData의 구독을 중단하고 새로운 LiveData의 구독을 시작하는 방식으로, 마지막 입력에 대한 결과만 UI에 반영할 수 있다.
class SearchViewModel : ViewModel() {
private val searchRepository = SearchRepository()
// 사용자 입력을 관리하는 StateFlow
private val _searchQuery = MutableStateFlow("")
// 검색 결과를 관리하는 Flow
val searchResults = _searchQuery
.debounce(300) // 사용자 입력 후 300ms 동안 대기
.filter { query ->
// 너무 짧은 검색어는 무시
query.length >= 3
}
.flatMapLatest { query ->
// 최신 검색어에 대한 검색 결과만 가져옴
searchRepository.getSearchResults(query)
}
.asLiveData() // UI에서 관찰하기 위해 LiveData로 변환
fun setSearchQuery(query: String) {
_searchQuery.value = query
}
}
플로우를 사용하게 되면 제공하는 기능 debounce로 쉽게 디바운싱 로직을 구현할수 있고
flatMapLatest 연산자는 각각의 입력에 대해 새로운 Flow를 생성하고, 새로운 입력이 들어올 때마다 이전에 생성된 Flow를 취소하고 최신 Flow의 결과만을 계속해서 방출하는데.
이는 특히 검색 기능 같이 사용자의 입력이 빠르게 변할 때 유용하며, 사용자가 마지막으로 입력한 검색어에 대한 결과만을 얻기 위해 사용된다.
StateFlow는 상태 관리에 최적화된 플로우인데 SharedFlow는 이벤트 관리에 최적화 되어 있다.
SharedFlow는 구성 가능한 버퍼링, 배압(backpressure) 전략, 그리고 중복 데이터 전송 제어 같은 고급 기능을 제공해 여러 구독자가 동일한 이벤트 시퀀스를 관찰할 수 있게 한다.
StateFlow나 SharedFlow를 구분하고 사용하기 위해선 상태와 이벤트를 명확히 구별할줄 알아야 한다.
둘의 차이는 쉽게 말해서 상태는 "현재 어떤 상태인가?"에 초점을 맞추며, 이벤트는 "무엇이 발생했나?"에 대한 반응을 다룬다
상태(State)
이벤트(Event)
class EventViewModel : ViewModel() {
// SharedFlow 설정: 최대 3개의 이벤트를 버퍼링하고, 추가 이벤트 발생 시 가장 오래된 이벤트를 드랍
private val _userEvents = MutableSharedFlow<String>(
replay = 0,
extraBufferCapacity = 3,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val userEvents: SharedFlow<String> = _userEvents.asSharedFlow()
fun sendUserEvent(event: String) {
viewModelScope.launch {
_userEvents.emit(event) // 이벤트 방출
}
}
}
MainActivity: 이벤트 구독 및 처리
class MainActivity : AppCompatActivity() {
private val eventViewModel: EventViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 첫 번째 구독자
lifecycleScope.launch {
eventViewModel.userEvents.collect { event ->
showToast("첫 번째 구독자: $event")
}
}
// 두 번째 구독자
lifecycleScope.launch {
eventViewModel.userEvents.collect { event ->
showToast("두 번째 구독자: $event")
}
}
}
private fun showToast(message: String) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}
}
이 예제에서는 SharedFlow를 사용하여 EventViewModel 내에서 이벤트를 발생시키고, MainActivity에서 이 이벤트를 구독하여 받은 메시지를 TextView에 표시하는 방식을 보여준다.
SharedFlow를 사용함으로써, 이벤트를 여러 구독자에게 동시에 방송할 수 있고, 각 구독자는 이벤트를 독립적으로 처리할 수 있다
이벤트의 버퍼링과 오버플로우 정책을 세밀하게 설정할 수 있어, 이벤트 스트림의 흐름을 효과적으로 제어할 수 있는 등 이벤트 처리에 특화 되어있다.