2023. 11. 29. 22:32ㆍ개발/[Kotlin] 안드로이드 개발
서론
LiveData가 클린 아키텍처적으로 Android 의존성을 띄기 때문에 domain layer에서 사용하기 부적합하는 등의 이유로 Flow를 대체적으로 많이 사용하고 있다. 여러모로 flow를 사용하면서 많은 레퍼런스도 참고 했지만 안드로이드에서 적용할 때 많이 아쉬운 부분이 있어서 flow 적용기를 남겨두려고한다.
이 글에서는 먼저 hot stream 과 cold stream의 차이를 알아보겠습니다.
본론
Cold Stream과 Hot Stream의 차이점은 3가지로 말할 수 있습니다.
1. 데이터가 생성되는 위치
2. 생산자가 발행한 데이터를 동시에 여러 소비자들이 수신할 수 있는지 여부
3. 스트림이 데이터를 생산하는 시점
Cold Stream(Flow)
1. 데이터가 생성되는 위치
Flow의 데이터는 내부에서 생성됩니다. 코드로 보겠습니다.
ViewModel
val count = flow {
for (i in 1 ..100) {
delay(1000L)
emit(i)
}
}
count 라는 변수는 flow scope 내부에서 데이터가 생성됩니다.
2. 생산자가 발행한 데이터를 동시에 여러 소비자들이 수신할 수 있는지 여부
여러 소비자가 flow 데이터를 구독할 수 있습니다.
하지만 하나의 소비자가 하나의 flow data를 구독할 때 flow scope가 실행되기 때문에 1:1 대응으로 독립적이게 됩니다. 코드로 보겠습니다.
ViewModel
val count = flow {
for (i in 1 ..100) {
delay(1000L)
emit(i)
}
}
Fragment
// 1번
CoroutineScope(Dispatchers.IO).launch {
boardViewModel.count.collect {
Log.d("count1", it.toString())
}
}
// 2번
CoroutineScope(Dispatchers.IO).launch {
boardViewModel.count.collect {
Log.d("count2", it.toString())
}
}
// 3번
CoroutineScope(Dispatchers.IO).launch {
delay(1500L)
boardViewModel.count.collect {
Log.d("count1", it.toString())
}
}
// 4번
CoroutineScope(Dispatchers.IO).launch {
boardViewModel.count.collect {
Log.d("count2", it.toString())
}
}
먼저 1, 2번 보겠습니다.
Fragment에서 ViewModel의 count 값을 구독합니다. cold stream의 경우 하나의 소비자가 collect할 때 독립적으로 내부에서 데이터가 발행되기 때문에 1,2번에서 1~100의 값이 로그에 찍히게 됩니다.
3, 4번 보겠습니다.
마찬가지로 독립적으로 데이터를 발행하기 때문에 delay(1500L)으로 세팅해도 3, 4번 둘 다 1~100의 값이 로그에 찍히게 됩니다.
3. 스트림이 데이터를 생산하는 시점
Cold Stream은 소비자가 소비를 시작할 때 데이터를 생산합니다.
ViewModel
val count = flow {
for (i in 1 ..100) {
delay(1000L)
emit(i)
}
}
Fragment
CoroutineScope(Dispatchers.IO).launch {
// 이 시점에서 Cold Stream의 데이터가 생산됩니다.
boardViewModel.count.collect {
Log.d("count2", it.toString())
}
}
collect 하는 시점에 flow 내부 블럭이 돌면서 데이터를 생산합니다.
Hot Stream(StateFlow, SharedFlow)
1. 데이터가 생성되는 위치
Hot Stream의 경우 외부에서 데이터가 생성됩니다. 코드로 보겠습니다.
val stateCount = flow {
for (i in 1 ..100) {
delay(1000L)
emit(i)
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000L),
initialValue = 99
)
네 이 코드는 flow stream을 stateFlow로 변환하는 코드 입니다.
StateFlow는 flow로 방출되고 있는 데이터를 stateFlow로 바꾸거나
ViewModel
private val _boardLocalUiState = MutableStateFlow<BoardLocalUiState<List<Board.Item>>>(BoardLocalUiState.Loading)
val boardLocalUiState = _boardLocalUiState.asStateFlow()
fun requestBoardLocalItem() = viewModelScope.launch {
_boardLocalUiState.value = BoardLocalUiState.Loading
runCatching {
requestBoardItemsFind()
}.onSuccess { items ->
_boardLocalUiState.value = items?.let {
BoardLocalUiState.Success(it)
} ?: BoardLocalUiState.Error("items is Null or Empty")
}.onFailure {
_boardLocalUiState.value = BoardLocalUiState.Error("requestBoardItemsFind Fail")
}
}
이렇게 value 속성을 사용해서 외부의 데이터를 넣어주는 방식으로 상태를 알고 있는 flow가 됩니다.
즉 외부에서 데이터를 넣어주거나 변환해준다 생각하시면 됩니다.
2. 생산자가 발행한 데이터를 동시에 여러 소비자들이 수신할 수 있는지 여부
Hot Stream은 생산자가 소비자의 소비 관계없이 생산합니다. 코드를 보겠습니다.
ViewModel
val stateCount = flow {
for (i in 1 ..100) {
delay(1000L)
emit(i)
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000L),
initialValue = 99
)
Fragment
CoroutineScope(Dispatchers.IO).launch {
delay(5500L)
boardViewModel.stateCount.collect {
Log.d("statecount1", it.toString())
}
}
CoroutineScope(Dispatchers.IO).launch {
boardViewModel.stateCount.collect {
Log.d("statecount2", it.toString())
}
}
이 말은 소비자 A와 소비자 B가 있을 때, B가 먼저 collect 해서 stateFlow 값의 구독을 하고
A는 5.5초 후에 구독을 시작합니다.
B의 경우 99, 1 , 2, 3, ~100의 값을 다 받을 수 있지만
A의 경우 5~100의 값을 받을 수 있습니다.
즉 stateFlow는 어떤 소비자든 구독을 시작하면 데이터를 발행하고 flow 와다르게 데이터를 처음부터 만드는 것이 아닌 최신 데이터를 방출합니다.
라디오를 생각하면 좀 쉬울것 같습니다. 1) 라디오는 회사가 라디오를 틀어놓죠? 2) 청취자가 해당 주파수로 맞추면 해당 라디오를 들을 수 있습니다. => 즉 데이터는 흘러나오고 있고 소비자가 구독할 때의 시점의 데이터를 받을 수 있다 라고 생각하면 됩니다.
3. 스트림이 데이터를 생산하는 시점
Hot Stream의 경우 소비자와 상관없이 데이터를 계속해서 생산한다는 말이 많습니다.
아니? 소비자가 구독을 하지 않는데 자동으로 변수가 값을 만든다고 ?!?!? 좀 의아했었는데요.
정확하게는 소비자가 구독을 시작할 때 값을 만들기 시작하고 다른 소비자가 구독할 때 데이터가 계속 발행되고 있기 때문에 A, B 소비자가 구독한 시간 차이만큼 옛날 데이터를 받지 못하는겁니다. 코드를 보겠습니다.
ViewModel
val stateCount = flow {
for (i in 1 ..100) {
delay(1000L)
emit(i)
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000L),
initialValue = 99
)
Fragment
CoroutineScope(Dispatchers.IO).launch {
delay(5500L)
boardViewModel.stateCount.collect {
Log.d("statecount1", it.toString())
}
}
CoroutineScope(Dispatchers.IO).launch {
delay(5500L)
boardViewModel.stateCount.collect {
Log.d("statecount2", it.toString())
}
}
소비자와 관계없이 데이터를 발행한다는 말을 듣고 다음과 같은 코드를 짜봤는데요.
5.5초 후에 구독을 시작하기 때문에 데이터 5부터 받겠지?라고 생각을 했는데요.
그렇지 않습니다 !
로그캣 보겠습니다.
2023-11-29 22:28:00.004 19083-19127 statecount1 D 99
2023-11-29 22:28:00.004 19083-19116 statecount2 D 99
2023-11-29 22:28:01.010 19083-19116 statecount1 D 1
2023-11-29 22:28:01.011 19083-19123 statecount2 D 1
2023-11-29 22:28:02.012 19083-19127 statecount1 D 2
2023-11-29 22:28:02.013 19083-19116 statecount2 D 2
2023-11-29 22:28:03.015 19083-19123 statecount1 D 3
2023-11-29 22:28:03.016 19083-19127 statecount2 D 3
2023-11-29 22:28:04.018 19083-19115 statecount2 D 4
2023-11-29 22:28:04.018 19083-19127 statecount1 D 4
2023-11-29 22:28:05.020 19083-19127 statecount1 D 5
2023-11-29 22:28:05.020 19083-19115 statecount2 D 5
네 이렇게 구독한 시점부터 데이터가 발행이 되는걸 확인할 수 있었습니다.
첫 구독이 시작된 이후에 데이터를 계속해서 방출되기 시작하고 소비자와 구독자가 1:1로 독립적이지 않은 1:N 관계로 독립적이지 않게 쭈욱 데이터를 방출하고 소비자는 구독한 시점에 데이터를 얻게 됩니다.
마치며
여기까지 Flow와 StateFlow를 통해 Cold Stream과 Hot Stream에 대해 알아봤습니다.
개인적으로 공부하면서 작성한 글이니 틀린 부분이 있으면 답글로 달아주시면 감사하겠습니다 !
'개발 > [Kotlin] 안드로이드 개발' 카테고리의 다른 글
[Android A..Z] Flow collect vs collectLatest (0) | 2024.03.30 |
---|---|
[Android A..Z] Flow StateFlow vs SharedFlow 비교 (0) | 2024.03.30 |
[Android A..Z] DI Dependency Injection 한 방에 끝내기 (0) | 2022.10.20 |
[Kotlin Flow] 예제를 활용해 쉽게 Flow에 대한 개념 익히기 -1- (0) | 2022.07.29 |
[Android A..Z] Asynchronous 비동기란? (0) | 2021.11.22 |