👩💻 오늘의 할 일
앞선 글에서 소개했듯이 프로젝트에 있던 LiveData들을 StateFlow로 대체하기 위해 공부하고 있습니다. 안드로이드에서는 어떤 이유로 Live Data를 StateFlow로 대체하라고 하는걸까요 ? StateFlow에 대해 알아보겠습니다.
📕 LiveData
public abstract class LiveData<T>
- LiveData란 Observable한 데이터 홀더 클래스입니다.
- Live Data는 Activity, Fragment, Service 등 다른 안드로이드 컴포넌트의 생명 주기를 따릅니다.
- 생명주기가 끝나는 즉시 Observing을 종료하고 삭제되어 Memory leak을 걱정하지 않아도 됩니다.
- 데이터 변화를 항상 관찰하며 Observer 객체에 알려 UI와 Data State의 일치를 보장할 수 있습니다.
Live Data는 ViewModel에서 데이터를 관리하고 Activity에선 이 데이터를 관찰하며 UI에 렌더링하는 용도로 프로젝트에서 사용 해왔습니다.
class MainViewModel: ViewModel() {
private val networkRepository = NetworkRepository()
private val _list = MutableLiveData<List<MyStoreInfoResponseModel>>()
val list: LiveData<List<MyStoreInfoResponseModel>> get() = _list
fun setAllStoreList() = viewModelScope.launch(Dispatchers.IO) {
try{
val response = networkRepository.getAllStoreInfo()
_list.postValue(response)
}catch (e : HttpException){
Log.d("MainViewModel", "404 Error Cause")
}
}
}
class StoreListFragment : Fragment() {
private var _binding: FragmentStoreListBinding ?= null
private val binding get() = _binding!!
private val viewModel: MainViewModel by viewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentStoreListBinding.inflate(inflater, container, false)
viewModel.setAllStoreList()
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.list.observe(viewLifecycleOwner){ list->
binding.selectRv.layoutManager = LinearLayoutManager(requireContext())
binding.selectRv.adapter = SelectStoreInfoAdapter(list)
}
}
}
이렇게만 보면 완벽한 라이브러리 처럼 보이지만 클린 아키텍쳐 관점에서 아래와 같은 단점이 있습니다.
LiveData는 Android 플랫폼에 종속적이고 UI가 없는 곳에서 LiveData를 사용하기가 어렵다.
클린 아키텍처에 간단히 서술하자면 안드로이드에서의 클린 아키텍쳐의 구조는 다음과 같이 나뉩니다.
1️⃣ UI(Presentation) Layer : View, ViewModel
2️⃣ Domain Layer : Repository(interface), UseCase
3️⃣ Data Layer : Repository(implement), DataSource
클린 아키텍쳐는 의존성 관계를 명확히 하여 유지 보수, 동작 구조 파악을 쉽게 하기 위함으로 아래와 같은 의존성을 가집니다.
UI -> Domain
Data -> Domain
📖 클린 아키텍쳐 관점에서 Live Dat의 문제점
1. LiveData의 Android platform 의존성
LiveData는 안드로이드 아키텍처 컴포넌트(AAC)로써, Android 플랫폼에 종속적이며 Android UI 컴포넌트들과 통합하기 위해 설계되었습니다. 그렇기 때문에 안드로이드 플랫폼에서만 쉽게 사용할 수 있습니다.
2. UI가 없으면 사용이 어렵다.
Presentation Layer에서는 LiveData가 잘 동작하지만 안드로이드 플랫폼에 독립적이고, 순수 Kotlin만 사용할 수 있는 즉, UI가 없고 언어 의존성만 지니는 Domain Layer에서는 LiveData를 쓰기 어렵습니다. 또한 Domain Layer에서는 비즈니스 로직에 집중되어야 하며 UI와 직접적으로 연결되는 Live Data를 사용하기는 적절하지 않습니다.
3. 계층 간 모듈화와 의존성 문제
안드로이드 의존성을 가진 LiveData를 사용하는 경우, 도메인 레이어에서 안드로이드 의존성을 갖게 되는데, 이는 클린 아키텍처에서 원하는 의존성 규칙을 깨는 결과로 이어질 수 있습니다. 특히, 안드로이드 의존성이 도메인 레이어로 스며들게 되면 안드로이드와 관련 없는 순수한 Kotlin 또는 Java 코드를 유지하기가 어려워집니다.
이런 이유로 안드로이드에서는 Coroutine이 발전하면서 LiveData를 대체하기 위해 Flow가 등장하게 되었고 더 나아가 StateFlow와 SharedFlow가 탄생하게 되었습니다.
📖 Flow의 한계
1. Flow는 상태가 없으며 Cold Stream 방식입니다.
Cold Stream 🧊 vs Hot Stream🔥
Cold Stream은 Flow를 수집하는 각각의 Collector 들이 데이터를 수집할 때 마다 새로운 데이터 스트림을 생성하므로 Collector 들은 각각의 개별적인 스트림에서 데이터를 수집합니다. 또한 스트림을 생성 후 이를 소비하지 않으면 아무런 동작을 하지 않습니다.
Hot Stream은 Flow를 수집하는 각각의 Collector들이 데이터 스트림을 공유해 동일한 데이터를 수집하며 기본적으로 Collector가 없어도 Producer는 데이터 스트림을 제공합니다.
Flow는 단순히 비동기 데이터 스트림을 나타내기 때문에 현재 값을 기억하거나 유지하지 않고 새로운 데이터가 생기면 그때마다 처리됩니다. 즉, 연속해서 들어오는 데이터를 처리할 수 없습니다.
이 예제를 보면 알 수 있듯이 Flow는 Cold Stream 방식으로 작동하므로 collect 함수가 실행되고 나서 값을 처음부터 수집해 출력합니다. 따라서 Flow는 현재 값을 유지하지 않고 데이터를 수집할 때 마다 새로운 값을 처리합니다.
저는 쉬운 예로 이해하기 위해 아래와 같이 생각했습니다.
- Cold Stream : 수도꼭지 -> 내가 틀어야 물이 나온다
- Hot Stream : 계곡 -> 내가 틀지 않아도 물이 계속 나온다
class StateFlowExample {
var stFlow: MutableStateFlow<Int> = MutableStateFlow(0)
suspend fun hot() {
for (i in 1..3) {
delay(100)
stFlow.value = i
}
}
fun cold(): Flow<Int> = flow {
println("Flow started")
for (i in 1..3) {
delay(100)
emit(i)
}
}
}
fun main() = runBlocking<Unit> {
val ex = StateFlowExample()
println("Calling flow...")
val flow = ex.cold()
println("Calling flow collect...")
flow.collect {
value -> println(value)
}
println("===========================")
println("Calling StateFlow...")
ex.hot()
println("Calling StateFlow collect...")
ex.stFlow.collect { value ->
println("StateFlow collected: $value")
}
}
2. Flow는 안드로이드의 생명주기에 알지 못합니다.
컴포넌트의 생명주기가 끝나면 제거되는 LiveData와는 달리 Flow는 생명주기를 인식하지 않기 때문에 생명주기 이벤트에 대한 처리가 필요한 경우 별도로 처리 해줘야 하는 번거로움이 있습니다.
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(State.STARTED) {
viewModel.data.collectLatest {
// handle UI
}
}
}
일반적인 Flow는 상태를 가질 수 없기 때문에 데이터 홀더의 역할을 하지 못합니다.
이런 한계점들을 보완하여 나온 것이 StateFlow와 SharedFlow 입니다.
🤔 StateFlow & SharedFlow
State - 객체가 특정 시점에 어떤 데이터를 가지고 있는지 나타내는 것으로, 객체의 특성이나 속성을 나타냄
StateFlow와 SharedFlow는 최적으로 State 데이터를 내보내고 여러 소비자에게 값을 보낼 수 있는 Flow API 입니다.
stateFlow도 LiveData와 비슷한 점이 있습니다. 둘 다 관찰 가능한 데이터 홀더 클래스입니다.
- 항상 값을 가지고 있으며 오직 하나의 값만을 가집니다. 즉, StateFlow는 항상 현재의 상태를 나타내며, 한 번 설정된 값은 계속 유지 되기 때문에 반드시 초기값이 존재해야 합니다.
- 여러 개의 수집자(collector)를 지원하며, flow를 공유할 수 있습니다. 다수의 수집자가 동일한 StateFlow를 동시에 관찰할 수 있으며 StateFlow는 모든 수집자에게 업데이트된 값을 제공합니다.
- Hot Stream 방식으로 동작해 새로운 값이 발생하면 해당 값이 수집자들에게 즉시 전달되므로 모든 구독자는 항상 최신 데이터를 유지할 수 있습니다.
- View가 STOPPED 상태가 되면 LivaData.observer( ) 는 Observer를 자동으로 등록을 취소하지만 StateFlow는 자동으로 Collect를 중지하지 않습니다. 동일한 동작을 실행하려면 LifeCycle.repeatOnLifecycle 블록에서 View의 생명주기를 수집해야 합니다.
// Activity
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED){
}
// Fragment
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED){
}
}
fun main() = runBlocking {
// 1. MutableStateFlow를 생성하고 초기값 "Initial State"로 설정
val testStateFlow = MutableStateFlow("hello StateFlow")
// 2. StateFlow를 수집하여 값 출력하는 함수
fun collectAndPrint() {
launch {
testStateFlow.collect { value ->
println("Collected: $value")
}
}
}
// 3. StateFlow를 수집하는 여러 collector를 생성
collectAndPrint()
collectAndPrint()
// 값을 업데이트하면 모든 collector에게 자동으로 새 값이 전달됨
testStateFlow.value = "Updated State 1"
// update 함수를 사용하여 현재 값을 기반으로 새 값을 계산하고 업데이트
testStateFlow.update { current ->
"New State based on $current"
}
// 값을 다시 업데이트하면 모든 collector에게 자동으로 새 값이 전달됨
testStateFlow.value = "Updated State 2"
// Coroutine이 완료될 때까지 대기
delay(1000)
}
SharedFlow는 값을 가지지 않으며 초기값을 갖고 있지 않아도 되며 이벤트라는 형태로 값을 전달하며 One-Time 이벤트에 매우 적합 합니다.
private val _sharedFlow = MutableSharedFlow<String>()
val sharedFlow = _sharedFlow.asSharedFlow()
fun triggerSharedFlow() {
viewModelScope.launch {
_sharedFlow.emit("SharedFlow")
}
}
이러한 Flow들은 아래와 같은 계층 구조를 가지고 있습니다.
Flow <- SharedFlow <- StateFlow
- StateFlow는 기존에 값과 같은 값을 emit 하지 않습니다.
- SharedFlow는 기존 값과 동등성에 상관없이 무조건 emit 합니다.
🙇♂️ 후기
프로젝트에서 LiveData를 정말 많이 사용했는데 이걸 대체 한다는게 정말 놀라웠습니다. Cold Stream, Hot Stream이란 말도 처음 들어서 아직 어색하고 SharedFlow 도 공부해야해서 갈 길이 멀지만 프로젝트에서 직접 사용하면서 StateFlow에 대해 익숙해지면 추후 SharedFlow에 대해서도 정리를 해볼려합니다.
'KOTLIN' 카테고리의 다른 글
Coroutine SharedFlow (0) | 2024.03.09 |
---|---|
StateFlow가 중복된 값을 반환하지 않는 이유(DistinctUntilChanged) (0) | 2024.03.08 |
[Kotlin] Coroutine Flow (0) | 2024.01.15 |
[KOTLIN IN DEPTH] 구조적 동시성과 코루틴 문맥 (1) | 2023.12.18 |
[KOTLIN IN DEPTH] Kotlin Coroutine Concurrency 2 (0) | 2023.12.18 |