코루틴 Flow vs StateFlow vs SharedFlow vs LiveData 총정리 하기
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. 예제 코드
이제 4가지 API의 차이점들에 대해서 보았으니,
간단한 예제들을 보면서 사용방법을 더 정리해 보겠습니다.
3-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
3-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
3-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에 대해서 비교해 보았습니다.