Android와 Kotlin의 코루틴이 계속 발전하면서,
리액티브(반응형) 프로그래밍을 구현한 다양한 API들이 많이 나왔습니다.
특히 데이터 스트림을 다루는
Flow, StateFlow, SharedFlow, LiveData는
비슷해 보이지만 각각 다른 특징을 가지고 있는데요.
오늘은 이 4가지 API를 철저히 비교해서 총정리해 보겠습니다.
1. Flow, StateFlow, SharedFlow, LiveData 표로 비교하기
Flow, StateFlow, SharedFlow, LiveData의 특징들을 살펴보고,
아래에서 표로 자세히 비교해 보겠습니다.
- Flow: 코루틴 기반의 거대한 API로 콜드(Cold) 스트림이며, Collect()될 때마다 새로운 스트림을 생성합니다.
- Backpressure도 지원하는 대표적인 reactive API
- StateFlow: 상태 관리에 최적화된 핫(Hot) 스트림.
- 항상 값을 가지고 있어, UI 상태나 데이터 상태를 관리하기에 적합(반드시 초기값이 필요)
- SharedFlow: 여러 수집자(Collector)들과 데이터를 공유하는데 특화된 핫(Hot) 스트림
- 세밀한 설정으로 다양한 시나리오에 대응할 수 있는 API
- replay 캐시를 통해 버퍼링이 가능
- LiveData: 안드로이드 플랫폼에 특화된 관찰 가능한 데이터 홀더
- 생명주기를 자동으로 인식하는 부분이 상대적 강점
특징 | Flow | StateFlow | SharedFlow | LiveData |
주요 용도 | 네트워크 요청, 데이터베이스 스트림 처리 등의 비동기 데이터 스트림 |
로딩 상태, 성공/실패 메시지, 현재 화면 상태, 폼 입력 상태 관리 등 UI 상태관리 |
네트워크 상태 변화, 알림, 사용자 액션 이벤트 처리 (여러 수집자에게 동시에 값 전달 가능) |
UI 상태 변경, (ViewModel에서 UI에 이벤트 전송) 폼 유효성 검사 Room과도 연동되어, Dao에서 반환타입으로 사용가능 |
초기값 필요여부 | 불필요 | 필수 | 선택적 | 선택적 |
스트림 유형 | Cold (구독 시작 시 데이터 생성 및 방출) |
Hot (collector가 없어도 동작) |
Hot | Hot |
값 갱신 | emit() | value 할당 | emit() | setValue() 또는 postValue() |
값에 접근 방법 |
collect() | value 속성, collect |
collect() | getValue() |
값 보유 | 없음 (스트림에서 흘러나가면 끝) |
최신 값 보유 (구독 연결시 즉시 최신 값 방출) |
기본적으로 보유하지 않지만, replay값을 설정해 보유 가능 |
최신 값 보유 |
중복 값 방출 | 가능 | 방출 안함 | 방출가능 | 방출가능 |
생명주기 인식 | 없음 | 없음 | 없음 | 있음 (생명주기 인식함) |
주요 용도 | 비동기 데이터 스트림 | 상태관리 | 이벤트 처리 | UI상태 처리 |
Collectors (수집자) 수 제한 여부 |
단일 | 다중 구독 가능 (필수임) |
다중 구독 가능 (Collector없어도 값을 방출) |
다중 |
버퍼링 | 없음 | 없음 | 설정 가능 | 없음 |
Backpressure 지원 |
지원 | 미지원 | 제한적인 지원 (extraBufferCapacity 등을 설정해 |
미지원 |
2. StateFlow vs LiveData
위에서 비교한 표를 보면,
StateFlow와 LiveData의 용도가 조금 겹치는 것을 볼 수 있습니다.
둘 다 현재 상태와 새로운 상태 업데이트를 Observer에게 전달해 주고요.
ViewModel과 View 사이의 데이터 통신에 사용되는 것도 같습니다.
코루틴 API에 중심을 두어,
suspend 함수들과 함께 사용하기가 자연스러운 StateFlow와
안드로이드에 종속적이면서 생명주기에 잘 융합된 LiveData,
어떤 걸 써야할까요?
다음 사항들을 기준으로 생각해 볼 수 있습니다.
- 초기값이 필수인 경우: StateFlow(LiveData는 필수 아님)
- 메모리 관리: LiveData는 항상 Lifecycle에 바인딩되야 하지만,
StateFlow는 필요한 경우에만 lifecycleScope에서 수집 - 테스트나 로직분리에 유리: StateFlow(안드로이드 프레임워크에 종속적이지 않아 유리)
써놓고 보니 추천드리는 것은 StateFlow이네요.
다만 아래 부분에서는 아직 LiveData가 우위에 있는 점이 있습니다.
- WorkManager에서는 LiveData직접 사용가능
- Room의 Dao클래스의 리턴타입으로 직접 사용가능
따라서 상황에 맞게 선택하는 것이 중요하겠지요.
3. StateFlow vs SharedFlow
StateFlow와 SharedFlow의 이름만 놓고 보면,
Collector(수집자)가 여러명일 경우, SharedFlow를 쓰면 되는 것 아닌가 할수도 있는데요.
사실 StateFlow도 여러 수집자가 collect할 수 있습니다.
단순히 Collector(수집자)의 수를 기준으로,
StateFlow냐, SharedFlow냐를 구분하지 않아야 합니다.
이 둘의 구분 방법에 대해서는 아래 글에서 자세히 다루었으므로,
아래 글을 참조해 주세요.
>> StateFlow vs SharedFlow 를 비교해보자 #이벤트 핸들링
4. 예제 코드
이제 4가지 API의 차이점들에 대해서 보았으니,
간단한 예제들을 보면서 사용방법을 더 정리해 보겠습니다.
4-1. Flow
Flow사용에 관한 간단한 예시입니다.
collect()함수를 호출하면,
스트림이 100ms의 딜레이를 두고 아이템을 흘려보내줍니다.
fun simpleFlow(): Flow<Int> = flow {
for (i in 1..8) {
delay(100)
emit(i)
}
}
fun main() = runBlocking {
simpleFlow().collect { value ->
println("Flow value: $value")
}
}
Flow()함수에 관한 자세한 내용은 아래 글을 참조해 주세요.
Kotlin Coroutine Flow 총정리 part3 # launchIn
4-2. StateFlow
이번에는 StateFlow를 사용한 간단한 예시를 보겠습니다.
일반적으로 외부에서는 값에 접근하지 못하도록,
MutableStateFlow와 StateFlow를 구분해서 사용합니다.
_counter는 내부 상태를 유지하고,
외부에서는 counter로 읽기전용 타입에만 접근하도록 하였습니다.
class CounterViewModel : ViewModel() {
private val _counter = MutableStateFlow(0)
val counter: StateFlow<Int> = _counter.asStateFlow()
fun increment() {
_counter.value++
}
}
runBlocking {
val viewModel = CounterViewModel()
launch {
viewModel.counter.collect { count ->
println("Count: $count")
}
}
delay(200)
viewModel.increment()
delay(200)
viewModel.increment()
}
StateFlow에 대해 더 깊이 알고 싶으시다면, 아래 글을 참조해 주세요.
>> StateFlow 정리 # Android Kotlin Coroutine getStateFlow StateIn
4-3. SharedFlow
핫스트림인 SharedFlow의 실제 코드를 보겠습니다.
class EventViewModel : ViewModel() {
private val _events = MutableSharedFlow<String>() // SharedFlow 선언
val events: SharedFlow<String> get() = _events
fun triggerEvent(eventMessage: String) {
viewModelScope.launch {
_events.emit(eventMessage) // 이벤트 방출
}
}
}
위와 같이 정의한 events에 Activity에서는 아래와 같이 접근합니다.
class EventActivity : AppCompatActivity() {
private lateinit var viewModel: EventViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_event)
viewModel = ViewModelProvider(this).get(EventViewModel::class.java)
// SharedFlow 수집
lifecycleScope.launchWhenStarted {
viewModel.events.collect { event ->
Toast.makeText(this@EventActivity, "Event: $event", Toast.LENGTH_SHORT).show()
}
}
findViewById<Button>(R.id.triggerButton).setOnClickListener {
viewModel.triggerEvent("Hello SharedFlow!") // 버튼 클릭 시 이벤트 트리거
}
}
}
SharedFlow에 대한 자세한 내용은 아래 글을 참조해 주세요.
SharedFlow 에 대한 총정리 # Buffer Replay tryEmit Kotlin Coroutine
3-4. LiveData
LiveData의 경우,
값을 갱신할 때 2가지 API를 사용합니다.
둘의 차이는 아래와 같습니다.
- setValue(): 메인 스레드(UI 스레드)에서 LiveData 값을 변경할 때 사용
- 값을 즉시 변경하고 getValue()를 통해 즉각적인 UI 업데이트가 필요한 경우에 적합
- postValue(): 백그라운드 스레드에서 LiveData 값을 변경할 때 사용
- 연속적인 값 변경이 있을 때, 최종 값만 적용하고 싶은 경우 적합
- 주의할 점: postValue()후, Main쓰레드에서 해당 task처리 전에, getValue()하면, 이전 값이 나올 수 있음
아래 코드에서도 캡슐화를 해서,
외부노출은 LiveData타입으로 하고,
내부에서 데이터 변경할 때는,
LiveData의 하위 클래스인 MutableLiveData를 사용하였습니다.
외부에서 상태 변화를 위해서는,
updateNote()함수를 이용하면 됩니다.
class NoteViewModel : ViewModel() {
private val _note = MutableLiveData<Note>()
val note: LiveData<Note> = _note
fun updateNote(title: String, content: String) {
_note.setValue(Note(title, content))
}
}
Activity에서는 아래와 같이 접근합니다.
updateNote()함수를 통해서 상태를 변경하고,
observe함수를 통해서 값을 얻어왔습니다.
class MainActivity : AppCompatActivity() {
private val viewModel: NoteViewModel by viewModels()
private lateinit var titleEditText: EditText
private lateinit var contentEditText: EditText
private lateinit var updateButton: Button
private lateinit var displayTextView: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
titleEditText = findViewById(R.id.titleEditText)
contentEditText = findViewById(R.id.contentEditText)
updateButton = findViewById(R.id.updateButton)
displayTextView = findViewById(R.id.displayTextView)
viewModel.note.observe(this) { note ->
displayTextView.text = "제목: ${note.title}\n내용: ${note.content}"
}
updateButton.setOnClickListener {
val title = titleEditText.text.toString()
val content = contentEditText.text.toString()
viewModel.updateNote(title, content)
}
}
}
이상으로 Flow, StateFlow, SharedFlow, LiveData에 대해서 비교해 보았습니다.
'Android 개발 > Coroutine , Flow, Channel' 카테고리의 다른 글
StateFlow vs SharedFlow 를 비교해보자 #이벤트 핸들링 (1) | 2023.05.06 |
---|---|
SharedFlow 에 대한 총정리 # Buffer Replay tryEmit Kotlin Coroutine (2) | 2023.05.04 |
Flow 결합연산자 combine , zip , merge 비교 총정리 # Kotlin Coroutine (0) | 2023.05.03 |
onEach vs onStart 비교 정리 # Kotlin Coroutine Flow (0) | 2023.05.03 |
Coroutine suspend 동작에 관한 좋은 예와 잘못된 예 # 비동기 (0) | 2023.04.18 |
flatMapLatest 이용해서 값이 들어오는 것을 기다리기 # Coroutine (0) | 2023.04.18 |
함수 실행 시간 측정 후 Delay 사용하기 (0) | 2023.04.14 |
MutableStateFlow 이용한 로딩 후 로딩 완료 기다리기 구현 방법 (0) | 2023.04.08 |
StateFlow 정리 # Android Kotlin Coroutine getStateFlow StateIn (0) | 2022.10.12 |
Kotlin Coroutine Flow 총정리 part3 # launchIn (0) | 2022.10.10 |
댓글