단일 값 vs 여러 값: suspend 함수는 단일 값만 반환하지만, Flow는 여러 값을 순차적으로 내보낼 수 있다
비동기적: 코루틴 위에서 동작하며, 값을 비동기적으로 생성하고 소비한다
논블로킹: 메인 스레드를 차단하지 않고 네트워크 요청 등의 작업을 안전하게 수행할 수 있다
Iterator와의 비교
Iterator와 유사하게 값들의 순서를 생성하지만, Flow는 suspend 함수를 활용하여 비동기적으로 동작한다는 점이 다르다.
데이터 스트림의 구성 요소
Flow 기반 데이터 스트림은 세 가지 주요 엔티티로 구성된다:
Producer(생산자): 스트림에 데이터를 생성하여 추가한다
코루틴을 활용하여 비동기적으로 데이터를 생성
예: Repository에서 네트워크/DB 데이터를 제공
Intermediaries(중개자, 선택적): 스트림의 데이터를 변환하거나 필터링한다
중간 연산자(map, filter, transform 등)를 사용
값을 소비하지 않고 가공만 수행
Consumer(소비자): 최종적으로 스트림의 값을 수집하여 사용한다
collect 등의 터미널 연산자를 사용
예: ViewModel에서 UI 상태를 업데이트
안드로이드 아키텍처에서의 역할
컴포넌트
역할
예시
Repository
UI 데이터의 생산자
네트워크/DB에서 데이터 제공
UI Layer
사용자 입력 이벤트의 생산자
클릭, 텍스트 입력 등
ViewModel
중개자 + 소비자
데이터 변환 및 UI 상태 관리
Flow 생성
Flow Builder API
Flow를 생성하는 가장 기본적인 방법은 flow { } 빌더를 사용하는 것이다.
class NewsRemoteDataSource(
private val newsApi: NewsApi,
private val refreshIntervalMs: Long = 5000
) {
val latestNews: Flow<List<ArticleHeadline>> = flow {
while(true) {
// 1. 네트워크에서 최신 뉴스 가져오기 (suspend 함수)
val latestNews = newsApi.fetchLatestNews()
// 2. 가져온 데이터를 Flow로 방출
emit(latestNews)
// 3. 지정된 시간만큼 대기 (코루틴 일시 중단)
delay(refreshIntervalMs)
}
}
}
interface NewsApi {
suspend fun fetchLatestNews(): List<ArticleHeadline>
}
Flow 빌더의 특성
Flow 빌더는 코루틴 내에서 실행되므로 비동기 API의 이점을 누릴 수 있지만, 다음과 같은 제약사항이 있다:
1. 순차적 실행
생산자가 코루틴에서 실행되므로, suspend 함수 호출 시 해당 함수가 완료될 때까지 대기한다
val numbers = flowOf(1, 2, 3, 4, 5)
// map: 각 값을 변환
numbers.map { it * 2 } // 2, 4, 6, 8, 10
// filter: 조건에 맞는 값만 통과
numbers.filter { it % 2 == 0 } // 2, 4
// transform: 복잡한 변환 수행 (0개 이상의 값 방출 가능)
numbers.transform { value ->
emit("String: $value")
emit("Double: ${value * 2}")
}
// take: 처음 N개만 가져오기
numbers.take(3) // 1, 2, 3
// distinctUntilChanged: 연속된 중복 값 제거
flowOf(1, 1, 2, 2, 3, 1).distinctUntilChanged() // 1, 2, 3, 1
Flow에서 데이터 수집
터미널 연산자(Terminal Operators)
Flow에서 실제로 값을 수집하려면 터미널 연산자를 사용해야 한다. 터미널 연산자는 Flow를 실제로 시작시키는 suspend 함수다.
collect
가장 기본적인 터미널 연산자로, 스트림에서 방출되는 모든 값을 수집한다.
class LatestNewsViewModel(
private val newsRepository: NewsRepository
) : ViewModel() {
init {
viewModelScope.launch {
// Flow에서 값을 수집하여 처리
newsRepository.favoriteLatestNews.collect { favoriteNews ->
// UI에 최신 뉴스 업데이트
updateUi(favoriteNews)
}
}
}
}
기타 유용한 터미널 연산자
val flow = flowOf(1, 2, 3, 4, 5)
// first(): 첫 번째 값만 가져오기
val first = flow.first() // 1
// single(): 단일 값 가져오기 (여러 값이 있으면 예외)
val single = flowOf(42).single() // 42
// toList(): 모든 값을 List로 수집
val list = flow.toList() // [1, 2, 3, 4, 5]
// reduce(): 값들을 누적하여 하나의 값으로 축약
val sum = flow.reduce { acc, value -> acc + value } // 15
다른 코루틴 컨텍스트에서 실행
flowOn 연산자 개요
기본적으로 Flow의 생산자는 collect를 호출하는 코루틴의 컨텍스트에서 실행된다. 하지만 Repository의 데이터 처리는 IO 스레드에서, UI 업데이트는 Main 스레드에서 실행하는 것이 일반적이다.
Upstream(업스트림): flowOn 이전의 생산자와 중간 연산자 → 지정된 Dispatcher에서 실행
Downstream(다운스트림): flowOn 이후의 중간 연산자와 소비자 → collect하는 컨텍스트에서 실행
여러 개의 flowOn을 사용하면, 각각 자신의 위치에서 업스트림 컨텍스트를 변경한다
사용 예제
class NewsRepository(
private val newsRemoteDataSource: NewsRemoteDataSource,
private val userData: UserData,
private val defaultDispatcher: CoroutineDispatcher
) {
val favoriteLatestNews: Flow<List<ArticleHeadline>> =
newsRemoteDataSource.latestNews
// ─── Upstream (defaultDispatcher에서 실행) ───
.map { news ->
// 사용자가 즐겨찾는 주제로 필터링
news.filter { userData.isFavoriteTopic(it) }
}
.onEach { news ->
// 캐시에 저장
saveInCache(news)
}
// flowOn 기준으로 위/아래가 나뉨
.flowOn(defaultDispatcher)
// ─── Downstream (collect하는 컨텍스트에서 실행) ───
.catch { exception ->
// 에러 발생 시 캐시된 데이터 제공
emit(lastCachedNews())
}
}
class NewsRemoteDataSource(
private val newsApi: NewsApi,
private val ioDispatcher: CoroutineDispatcher
) {
val latestNews: Flow<List<ArticleHeadline>> = flow {
// 네트워크 요청 - IO Dispatcher에서 실행됨
val news = newsApi.fetchLatestNews()
emit(news)
}.flowOn(ioDispatcher) // Flow 생산자를 IO Dispatcher로 이동
}
베스트 프랙티스:
DataSource 레이어: I/O 작업 → Dispatchers.IO 사용
Repository 레이어: 데이터 가공 → Dispatchers.Default 사용
ViewModel 레이어: UI 업데이트 → Dispatchers.Main에서 collect
콜백 기반 API를 Flow로 변환
callbackFlow
콜백 기반 API(Firebase, 위치 서비스 등)를 Flow로 변환할 때 사용하는 Flow 빌더다.
class FirestoreUserEventsDataSource(
private val firestore: FirebaseFirestore
) {
// Firestore database에서 user events를 가져오는 함수
fun getUserEvents(): Flow<UserEvents> = callbackFlow {
// 1. Firestore 컬렉션 레퍼런스 가져오기
var eventsCollection: CollectionReference? = null
try {
eventsCollection = FirebaseFirestore.getInstance()
.collection("collection")
.document("app")
} catch (e: Throwable) {
// Firebase 초기화 실패 시 스트림 닫기
close(e)
}
// 2. Firestore에 콜백 리스너 등록
val subscription = eventsCollection?.addSnapshotListener { snapshot, _ ->
if (snapshot == null) { return@addSnapshotListener }
// 3. 새로운 이벤트를 Flow로 전송
// trySend: 코루틴 외부에서도 값을 방출 가능
try {
trySend(snapshot.getEvents()).isSuccess
} catch (e: Throwable) {
// 에러 처리
}
}
// 4. Flow가 취소되면 리스너 제거
// awaitClose: Flow가 닫힐 때까지 대기하다가 정리 작업 수행
awaitClose { subscription?.remove() }
}
}
callbackFlow vs flow
특징
flow
callbackFlow
컨텍스트 변경
불가능 (withContext 사용 불가)
가능
값 방출 함수
emit (suspend 함수)
send/trySend (일반 함수)
사용 사례
일반적인 비동기 작업
콜백 기반 API 변환
버퍼링
없음
있음 (설정 가능)
StateFlow와 SharedFlow
StateFlow 개요
StateFlow는 Hot Flow의 한 종류로, 현재 상태를 유지하고 관찰 가능한 Flow다.
주요 특징:
항상 최신 값을 하나 보유하고 있다
새로운 collector가 구독하면 즉시 현재 값을 전달받는다
동일한 값은 방출하지 않는다 (값이 실제로 변경되었을 때만 방출)
Android에서 UI 상태 관리에 이상적이다
Cold Flow vs Hot Flow 비교
Cold Flow
flow { ... } 빌더로 만든 대부분의 Flow
collect()가 호출되기 전까지는 아무것도 하지 않음.
collect()가 호출되면, 그때서야 Flow가 활성화되어 값을 생성하고 방출함.
Hot FLow
StateFlow와 SharedFlow가 여기 속함.
collect()하는 대상이 있든 없든, Flow 자체가 이미 활성화되어 있음.
값을 생성하거나 이벤트를 받을 준비가 항상 되어있음.
구분
Cold Flow
Hot Flow (StateFlow)
생성 방식
flow { }
MutableStateFlow()
실행 시점
collect 시 생산자 코드 실행
즉시 활성화
구독자 수
각 collector마다 독립적 실행
모든 collector가 같은 인스턴스 공유
초기값
없음
필수
사용 사례
일회성 작업, 네트워크 요청
UI 상태, 실시간 데이터
Hot Flow의 기준은 "값을 보유하는가"가 아니라, "Collector(수집가)가 없어도 활성화되어 있는가"이다.
StateFlow 사용 예제
class LatestNewsViewModel(
private val newsRepository: NewsRepository
) : ViewModel() {
// private 변경 가능한 StateFlow
private val _uiState = MutableStateFlow<LatestNewsUiState>(
LatestNewsUiState.Success(emptyList())
)
// public 읽기 전용 StateFlow (다운캐스팅 방지)
val uiState: StateFlow<LatestNewsUiState> = _uiState.asStateFlow()
init {
viewModelScope.launch {
newsRepository.favoriteLatestNews
.collect { favoriteNews ->
// UI 상태 업데이트
_uiState.value = LatestNewsUiState.Success(favoriteNews)
}
}
}
}
sealed class LatestNewsUiState {
data class Success(val news: List<ArticleHeadline>): LatestNewsUiState()
data class Error(val exception: Throwable): LatestNewsUiState()
}
Android에서 안전하게 collect하기
잘못된 방법:
class NewsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 위험: Activity가 백그라운드에 있어도 계속 수집함
lifecycleScope.launch {
viewModel.uiState.collect { state ->
updateUi(state) // 크래시 가능!
}
}
}
}
올바른 방법:
class NewsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// repeatOnLifecycle: STARTED 상태일 때만 수집
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
updateUi(state) // 안전!
}
}
}
}
}
Flow를 StateFlow로 변환: stateIn
class NewsRepository(
private val newsRemoteDataSource: NewsRemoteDataSource
) {
// Cold Flow를 Hot StateFlow로 변환
val latestNews: StateFlow<List<ArticleHeadline>> =
newsRemoteDataSource.latestNews
.stateIn(
scope = CoroutineScope(Dispatchers.Default),
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
}
stateIn 파라미터:
scope: StateFlow가 활성화될 코루틴 스코프
started: 구독 시작/중지 전략
Eagerly: 즉시 시작, 스코프가 취소될 때까지 유지
Lazily: 첫 구독자가 나타나면 시작, 영구 유지
WhileSubscribed(timeout): 구독자가 있을 때만 활성, timeout 후 중지
initialValue: 초기 상태 값
SharedFlow란?
SharedFlow는 StateFlow와 유사한 Hot Flow지만, 상태를 보유하지 않고 이벤트를 브로드캐스트한다.
StateFlow vs SharedFlow:
특징
StateFlow
SharedFlow
초기값
필수
선택적
현재 값 보유
항상 최신 값 유지
값 보유하지 않음
중복 값 방출
동일한 값 무시
모든 값 방출
Replay
1 (최신 값만)
0~N (설정 가능)
사용 사례
UI 상태
일회성 이벤트 (토스트, 네비게이션)
class NewsViewModel : ViewModel() {
// StateFlow: UI 상태
private val _newsState = MutableStateFlow<NewsState>(NewsState.Loading)
val newsState: StateFlow<NewsState> = _newsState.asStateFlow()
// SharedFlow: 일회성 이벤트
private val _navigationEvent = MutableSharedFlow<NavigationEvent>()
val navigationEvent: SharedFlow<NavigationEvent> = _navigationEvent.asSharedFlow()
fun onArticleClick(articleId: String) {
viewModelScope.launch {
// 네비게이션 이벤트 발생 (한 번만 처리되어야 함)
_navigationEvent.emit(NavigationEvent.ToArticleDetail(articleId))
}
}
}
sealed class NavigationEvent {
data class ToArticleDetail(val id: String) : NavigationEvent()
}
UI 이벤트: Channel vs SharedFlow
ViewModel에서 일회성 UI 이벤트(토스트, 네비게이션, 스낵바 등)를 처리할 때 Channel과 SharedFlow 중 어떤 것을 사용해야 할까?
Channel 방식
class NewsViewModel : ViewModel() {
private val _uiEvent = Channel<UiEvent>()
val uiEvent = _uiEvent.receiveAsFlow()
fun onSaveClick() {
viewModelScope.launch {
try {
saveNews()
_uiEvent.send(UiEvent.ShowToast("저장 완료"))
} catch (e: Exception) {
_uiEvent.send(UiEvent.ShowError(e.message))
}
}
}
}
// UI에서 수집
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiEvent.collect { event ->
when (event) {
is UiEvent.ShowToast -> showToast(event.message)
is UiEvent.ShowError -> showError(event.message)
}
}
}
}
SharedFlow 방식
class NewsViewModel : ViewModel() {
private val _uiEvent = MutableSharedFlow<UiEvent>()
val uiEvent = _uiEvent.asSharedFlow()
fun onSaveClick() {
viewModelScope.launch {
try {
saveNews()
_uiEvent.emit(UiEvent.ShowToast("저장 완료"))
} catch (e: Exception) {
_uiEvent.emit(UiEvent.ShowError(e.message))
}
}
}
}
// UI에서 수집 (동일한 방식)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiEvent.collect { event ->
when (event) {
is UiEvent.ShowToast -> showToast(event.message)
is UiEvent.ShowError -> showError(event.message)
}
}
}
}
Channel vs SharedFlow 비교
특징
Channel
SharedFlow
구독자 수
단일 구독자 (먼저 수집하는 쪽이 소비)
여러 구독자 가능 (모두에게 브로드캐스트)
이벤트 손실
구독 전 이벤트 손실 가능
구독 전 이벤트 손실 가능 (replay=0)
이벤트 소비
한 번만 소비됨 (큐 방식)
모든 구독자가 받음 (브로드캐스트)
버퍼 전략
다양한 버퍼 전략 지원
extraBufferCapacity로 설정
타입
Flow가 아님 (Channel)
Flow 타입
사용 난이도
간단
약간 복잡 (replay, extraBufferCapacity 설정 필요)
어떤 것을 사용해야 할까?
Channel을 사용하는 경우:
// 단일 화면에서만 이벤트를 처리하는 경우
class ProfileViewModel : ViewModel() {
private val _events = Channel<ProfileEvent>()
val events = _events.receiveAsFlow()
// 장점: 간단하고 직관적
// 단점: 화면 회전 시 이벤트 손실 가능
}
SharedFlow를 사용하는 경우:
// 여러 구독자가 있거나 더 세밀한 제어가 필요한 경우
class ProfileViewModel : ViewModel() {
private val _events = MutableSharedFlow<ProfileEvent>(
replay = 0, // 새 구독자에게 재방출하지 않음
extraBufferCapacity = 1, // 구독자가 없어도 1개 이벤트 버퍼링
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val events = _events.asSharedFlow()
// 장점: 여러 구독자 지원, 세밀한 제어 가능
// 단점: 설정이 복잡함
}
권장 사항
시나리오
권장 방식
이유
단일 Activity/Fragment
Channel
간단하고 충분함
여러 구독자 필요
SharedFlow
브로드캐스트 지원
화면 회전 시 이벤트 보존
SharedFlow (replay=1)
최근 이벤트 재전달
간단한 일회성 이벤트
Channel
코드가 더 간결함
일반적인 베스트 프랙티스:
간단한 경우: Channel 사용 (대부분의 경우)
복잡한 요구사항: SharedFlow 사용 (여러 구독자, 이벤트 버퍼링 등)
sealed class UiEvent {
data class ShowToast(val message: String) : UiEvent()
data class ShowError(val message: String?) : UiEvent()
data class Navigate(val route: String) : UiEvent()
}
정리
선택 가이드
어떤 방식을 사용해야 할까?
단일 값만 필요한가?
↓ YES
suspend 함수 사용
↓ NO
여러 값을 비동기로 방출해야 하는가?
↓ YES
현재 상태를 유지해야 하는가?
↓ YES → StateFlow 사용 (UI 상태)
↓ NO
일회성 UI 이벤트인가?
↓ YES
여러 구독자가 필요한가?
↓ YES → SharedFlow 사용
↓ NO → Channel 사용 (더 간단)
↓ NO
여러 구독자에게 브로드캐스트해야 하는가?
↓ YES → SharedFlow 사용
↓ NO → Flow 사용 (일반 스트림)