
코틀린 Flow와 suspend 함수 차이는 문법보다 먼저 계약으로 이해하는 편이 훨씬 쉽습니다. 한 번 결과를 받고 끝나는 작업이면 suspend 함수가 더 자연스럽고, 시간에 따라 여러 값이 계속 들어오는 작업이면 Flow가 더 잘 맞습니다.
핵심은 비동기인가 아닌가가 아닙니다. 둘 다 비동기 작업을 다룰 수 있지만, 진짜 차이는 결과가 한 번 나오고 끝나는지, 아니면 여러 번 흘러오는지에 있습니다.
코틀린 Flow와 suspend 함수 차이를 먼저 한 문장으로 잡자
suspend 함수는 비동기적으로 한 번 계산해서 결과를 돌려주는 쪽에 가깝습니다. 반면 Flow는 비동기적으로 여러 값을 순서대로 흘려보내는 데이터 스트림에 가깝습니다.
- suspend 함수: 음식점에서 주문한 메뉴 한 번 받기
- Flow: 배달 위치가 계속 업데이트되는 지도 보기
주문 상세 조회처럼 한 번 응답을 받으면 끝나는 작업도 있고, 채팅 메시지나 센서 값처럼 시간이 지나며 값이 계속 들어오는 작업도 있습니다. 질문 자체가 다르면 반환 타입도 달라지는 편이 자연스럽습니다.
suspend 함수는 호출하면 그 작업을 수행하고 한 번 끝난다
Kotlin 공식 문서 기준으로 코루틴 안의 코드는 일반 코드처럼 기본적으로 순차적으로 실행됩니다. 그래서 suspend 함수는 비동기 작업을 하더라도 호출자가 그 결과를 한 번 받아 다음 단계로 넘어가는 모델과 잘 맞습니다.
suspend fun fetchUserProfile(userId: Long): UserProfile {
delay(300)
return api.getUserProfile(userId)
}
suspend fun loadScreen(userId: Long): ScreenState {
val profile = fetchUserProfile(userId)
return ScreenState(profile = profile)
}- 단건 네트워크 요청
- 파일 저장 한 번
- 토큰 갱신 한 번
- 계산 결과 한 번 반환
이런 작업은 함수를 호출하면 작업이 수행되고, 끝나면 결과 하나가 돌아온다는 계약으로 충분한 경우가 많습니다. 이때는 Flow보다 suspend 함수가 더 단순하고 읽기 쉽습니다.
Flow는 여러 값을 흘려보내는 쪽에 더 가깝다
Flow는 비동기 데이터 스트림입니다. 결과를 한 번만 반환하는 것이 아니라 시간이 지나면서 값을 순서대로 emit할 수 있습니다.
fun observeMessages(roomId: String): Flow<Message> = flow {
while (true) {
val next = messageSource.waitNext(roomId)
emit(next)
}
}- 값이 하나로 끝나지 않을 수 있다
- 호출자는 collect하면서 여러 값을 받게 된다
- 값이 들어오는 동안 수집이 계속 유지될 수 있다
즉, Flow는 시간 축을 가진 데이터 계약에 가깝습니다. 여러 값이 흘러오는 성격이 없다면 Flow를 쓸 이유도 약해집니다.
많은 혼란은 둘 다 비동기라는 공통점 때문에 생긴다
둘 다 코루틴 안에서 쓰이기 때문에 대체 관계처럼 보이지만, 실제로는 호출자에게 약속하는 결과 모델이 다릅니다. suspend 함수는 나중에 결과 하나를 주고, Flow는 나중에 결과를 여러 번 줄 수 있습니다.
이 차이를 놓치면 repository나 use case의 반환 타입이 흐려지고, 호출하는 쪽도 언제 작업이 시작되는지, 언제 끝나는지, 몇 번 값이 올 수 있는지 예측하기 어려워집니다.
Flow는 cold라서 함수 호출 시점이 아니라 collect 시점이 중요하다
기본적인 Flow는 cold stream입니다. 즉, flow builder 안의 코드는 Flow를 만드는 순간 바로 실행되지 않고, 실제로 collect될 때 시작됩니다.
fun fetchNumbers(): Flow<Int> = flow {
println("Flow started")
emit(1)
emit(2)
emit(3)
}
suspend fun demo() {
val numbers = fetchNumbers()
println("아직 collect 전")
numbers.collect { value ->
println(value)
}
}이 특성 때문에 같은 Flow를 두 번 collect하면 upstream 작업도 두 번 다시 시작될 수 있습니다. 값의 개수뿐 아니라 시작 시점까지 호출자에게 함께 약속한다는 점이 Flow 설계의 핵심입니다.
suspend 함수는 호출 시 실행되는 한 번짜리 작업에 가깝고, 기본 Flow는 collect 시 실행되는 스트림 계약에 가깝습니다.
intermediate operator를 붙였다고 실행되는 것은 아니다
Flow의 map, filter 같은 intermediate operator는 실행 체인을 준비할 뿐이고, 실제 실행은 terminal operator에서 시작됩니다. 즉, collect, single, toList 같은 단계가 있어야 upstream 연산이 수행됩니다.
val namesFlow = userFlow
.filter { it.isActive }
.map { it.name }이 점을 이해하면 지금 필요한 것이 즉시 한 번 수행되는 작업인지, 아니면 나중에 누군가 수집할 때마다 다시 시작될 수 있는 정의인지를 더 정확히 구분할 수 있습니다.
같은 기능이라도 질문이 달라지면 반환 타입이 달라진다
예를 들어 사용자 정보를 다루더라도 지금 이 순간 상세를 한 번 가져오라는 질문이면 suspend 함수가 더 자연스럽고, 사용자 정보가 바뀔 때마다 계속 알려달라는 질문이면 Flow가 더 자연스럽습니다.
suspend fun fetchOrderDetail(orderId: String): OrderDetail
fun observeOrderStatus(orderId: String): Flow<OrderStatus>첫 번째는 지금 주문 상세를 한 번 받는 작업이고, 두 번째는 배송 준비, 출발, 도착처럼 시간이 흐르며 바뀌는 상태를 계속 관찰하는 작업입니다. 같은 반환 타입으로 억지로 맞추기보다 질문의 성격을 먼저 나누는 편이 좋습니다.
Flow를 남용하면 단건 작업까지 불필요하게 collect하게 된다
모든 비동기 API를 Flow로 통일하면 겉보기에는 깔끔해 보여도, 단건 작업에도 collect가 필요해져 호출 계약이 불필요하게 무거워질 수 있습니다.
suspend fun login(email: String, password: String): LoginResult로그인처럼 결과가 한 번만 필요한 작업이라면 이 정도 계약으로 충분한 경우가 많습니다. 이 작업을 Flow로 만들면 재수집 시 다시 로그인 요청이 나가는지까지 호출자가 함께 신경 써야 합니다.
계속 바뀌는 데이터를 suspend 함수로만 누르면 호출자가 반복 polling을 떠안게 된다
반대로 계속 변하는 데이터를 suspend 함수로만 주면 호출자가 같은 함수를 반복 호출하며 변화를 감시해야 할 수 있습니다. 이때는 polling 간격, 중복 호출 비용, 변경 감지 책임이 호출자 쪽으로 밀리기 쉽습니다.
예를 들어 채팅방의 새 메시지를 계속 보여줘야 하는데 latestMessages를 반복 호출하게 만들면, 실제로는 스트림 문제를 단건 API로 억지로 풀게 됩니다. 이런 장면에서는 Flow가 문제 구조에 더 잘 맞습니다.
안드로이드에서는 collect timing이 더 중요하게 드러난다
안드로이드에서는 화면이 보일 때만 수집해야 하는 데이터도 있고, 다시 collect할 때 upstream 작업이 다시 시작될 수 있기 때문에 Flow의 collect timing이 특히 중요합니다.
- 이 Flow를 다시 collect하면 upstream 작업을 다시 해도 괜찮은가
- 수집 중단과 재수집이 자연스러운 데이터인가
- 화면 생명주기와 함께 붙였다 뗐다 해도 의미가 유지되는가
이 감각은 repeatOnLifecycle이나 collectLatest 같은 패턴으로 이어집니다. 하지만 그보다 먼저, 왜 그런 수집 패턴이 필요한지를 Flow의 cold 특성으로 이해하는 것이 더 중요합니다.
가장 실전적인 API 설계 기준
- 결과가 한 번 나오고 끝나는가
- 시간이 지나면서 값이 여러 번 바뀌는가
- 작업 시작 시점을 호출 시점으로 보는가, collect 시점으로 보는가
- 같은 소스를 다시 수집해도 자연스러운가
- 1번이 yes면 suspend 함수 쪽이 더 자연스럽다
- 2번이 yes면 Flow 쪽을 먼저 검토한다
- collect 시점 시작이 중요하면 Flow 가능성이 높다
- 재수집이 어색하면 hot stream이나 다른 설계를 같이 검토해야 한다
특히 이 API는 결과를 한 번 건네는가, 아니면 관찰 관계를 여는가라는 질문이 가장 강력합니다. 이 차이를 분명히 하면 반환 타입 선택이 훨씬 쉬워집니다.
자주 하는 오해
Flow가 suspend 함수의 상위 버전이라는 오해
그렇지 않습니다. Flow는 더 범용적인 장비가 아니라 다른 계약입니다. 단건 작업을 모두 Flow로 감싸면 호출자에게 불필요한 수집 책임을 넘길 수 있습니다.
suspend 함수는 스트림을 다룰 수 없다는 오해
스트림 소스를 내부적으로 사용하더라도 외부 계약을 한 번짜리 결과로 마무리하고 싶다면 suspend 함수로 감쌀 수 있습니다. 중요한 것은 구현 내부가 아니라 호출자에게 어떤 약속을 하느냐입니다.
Flow를 반환하면 자동으로 효율적이라는 오해
기본 Flow는 cold입니다. 따라서 collect할 때마다 upstream가 다시 실행될 수 있습니다. 재수집 비용과 시작 시점을 생각하지 않으면 오히려 호출 횟수와 작업 비용을 숨겨버릴 수 있습니다.
마무리
코틀린 Flow와 suspend 함수 차이는 결국 결과 개수와 시작 시점의 차이입니다. suspend 함수는 한 번 실행해서 결과 하나를 돌려주는 작업에 잘 맞고, Flow는 collect될 때 시작되어 여러 값을 순서대로 흘려보내는 작업에 잘 맞습니다.
그래서 질문은 이것이면 충분합니다. 이 작업은 한 번 답하면 끝나는가, 아니면 계속 관찰해야 하는가.
Kotlin 코루틴 흐름을 더 이어서 보고 싶다면 coroutineScope와 supervisorScope 차이, 안드로이드에서 상태와 이벤트를 나누는 흐름까지 연결하고 싶다면 StateFlow와 SharedFlow 차이, Flow 수집 타이밍을 lifecycle과 함께 보고 싶다면 repeatOnLifecycle vs launchWhenStarted, 이전 작업 취소 기준까지 이어서 보고 싶다면 collectLatest와 collect 차이도 함께 보면 좋습니다.
공식 기준은 Kotlin Flow 문서, Flow API 문서, suspending function composition 문서를 같이 읽어보면 가장 정확합니다.