시간의 흐름에 따라 여러 개의 데이터가 연속적으로, 비동기적으로 발생하는 상황을 '데이터 스트림(Data Stream)'이라고 부릅니다. suspend 함수만으로는 이런 스트림을 처리하기가 까다롭습니다.
데이터 스트림을 코루틴안에서 효율적으로 다루기 위해 등장한 것이 Flow입니다.
Flow의 세 가지 핵심 역할
1. 생산자 (Producer) : 데이터를 만들어 내보내는 역할
flow { ... } 빌더를 사용해서 데이터 스트림을 만듭니다. emit() 함수를 사용해서 데이터를 하나씩 밖으로 내보냅니다.
// 1초마다 숫자를 0, 1, 2 ... 순서대로 발행(emit)하는 Flow
fun countNumbers(): Flow<Int> = flow {
var count = 0
while (true) {
emit(count) // 데이터(count)를 흘려보낸다
count++
delay(1000) // 1초 대기
}
}
2. (중간) 연산자 (Intermediate Operator) : 흐르는 데이터를 가공하는 역할
생산자가 보낸 데이터가 소비자에게 도달하기 전에 중간에서 데이터를 원하는 대로 가공할 수 있습니다.
map, filter처럼 컬렉션에서 자주 사용하던 익숙한 연산자들입니다.
countNumbers()
.filter { it % 2 == 0 } // 짝수만 통과시키는 필터
.map { "숫자는 바로 $it 입니다!" } // 숫자를 문자열로 가공
연산자는 '이렇게 처리할 것이다'라고 계획만 세워두는 것입니다.
실제로 데이터를 흘려보내고 처리하는 작업은 소비자가 구독을 시작해야만 실행됩니다. 이를 'Cold Stream'이라고 부릅니다.
3. 소비자 (Consumer) : 데이터를 받아서 사용하는 역할
collect라는 종단 연산자(Terminal Operator)를 사용해서 Flow가 발생하는 데이터를 받기 시작합니다.
collect가 호출되는 순간, 생산자는 데이터를 emit하기 시작하고, 중간 연산자는 가공을 시작합니다.
// viewModelScope 안에서 실행했다고 가정
viewModelScope.launch {
countNumbers()
.filter { it % 2 == 0 }
.map { "숫자는 바로 $it 입니다!" }
.collect { message -> // collect를 호출하는 순간부터 Flow가 동작 시작!
// "숫자는 바로 0 입니다!"
// (2초 후) "숫자는 바로 2 입니다!"
// (2초 후) "숫자는 바로 4 입니다!"
// ... 계속 UI에 메시지를 표시
_myUiState.value = message
}
}
안드로이드 개발자가 Flow를 좋아하는 이유
구조화된 동시성
Flow는 코루틴 스코프(lifecycleScope, viewModelScope)안에서 동작하기 때문에, 화면이 사라지는 등 스코프가 취소되면 Flow도 자동으로 함께 취소됩니다. 불필요한 작업이나 메모리 누수를 막아서 안전합니다.
백프레셔(Back-pressure)지원
만약 생산자가 데이터를 빨리 만들고 소비자가 처리하는 속도가 느리다면 어떻게 될까요? Flow는 이런 상황을 알아서 처리 해 주는 똑똑한 기능 (백프레셔)을 내장하고 있습니다. 앱이 갑자기 정지되거나 메모리가 누수 되는 문제를 방지해줍니다.
풍부한 연산자
map, filter 외에도 여러 Flow를 합치거나(zip, combin), 특정 시간 동안 들어온 값 중 마지막 값만 처리하는(debounce)등 실무에 유용한 수많은 연산자를 제공합니다.
Jetpack 라이브러리와 환성적인 궁합
Room, DataStore, Retrofit 등 많은 라이브러리들이 이제 Flow를 지원합니다. 데이터베이스에 변화가 생기면 자동으로 Flow를 통해 알려줍니다.
코루틴이 비동기 프로그래밍의 기본기라면, Flow는 그 안에서 연속적인 데이터를 자유자재로 다루는 응용기술이라고 할 수 있습니다.