Android/학습

[Android] Kotlin Flow의 first (with. Cold Steam, Hot Steam)

한때미 2025. 11. 9. 00:59

프로젝트를 리딩 하다 보면, 이런 류의 코드를 종종 볼 때가 있다.

var tmp: Int = -1
flowData.collect {
    if (it > 3) {
    	tmp = it
        cancel()
    }
}

 

대충 이런 코드를 보게 되면 고치고 싶다는 생각이 든다.

왜냐면 Flow에는 해당 로직을 더 간결하게 표현해 줄 좋은 연사자들이 많이 존재하기 때문이다.

first()
filter { }

 

원래 잘 돌아가던 로직을 고치기 위해서는 용기가 아닌 지식이 필요하다.

과연 기존 로직과 동일하게 동작할까? 좀 더 자세히 알아보자.

 


Flow.first()

suspend fun <T> Flow<T>.first(): T

흐름에서 방출된 첫 번째 요소를 반환하고 흐름의 수집을 취소하는 터미널 연산자입니다. 

흐름이 ​​비어 있으면 NoSuchElementException을 throw 합니다.

 

 

first()는 값의 첫 번째 값을 가져오는 기능을 한다. 하지만 Flow의 first() 정의에 마음에 걸리는 문구가 있다.

흐름(flow)이 ​​비어 있으면 NoSuchElementExceptionthrow 합니다.

 

에러발생시킨다고? 에러를 언제 발생시키는데?


만약에, 만약에 말이다.

flowData.filter { it > 3 }.first()

이런 로직으로 수정을 했을 때, 5라는 데이터가 존재하지 않아서 filter에 해당 데이터가 계속 넘어가지 않는다면.. 앱이 터져버리는 게 아닐까? 잘 돌아가던 앱이 심각한 오류를 발생시켜 버리는 거 아니야??

 

 

과련 flow의 first()는 언제 "값이 없음"을 판단하는 것일까?

결론부터 말하자면 flow(steam)가 끝날 때이다.

 


우선, cold Steam, Flow에 대해서만 살펴보도록 하자.

여기 4 종류의 Flow가 있다.

  • getNotDataFlow(): 조건에 맞는 데이터가 존재하지 않는 Flow
  • getNotDataDelayFlow(): 조건에 맞지 않는 데이터가 시간 간격을 두고 emit 되는 Flow
  • getFlow(): 조건에 맞는 데이터가 존재하는 Flow
  • getDelayFlow(): 조건에 맞는 데이터가 시간 간격을 두고 emit 되는 Flow
fun getNotDataFlow() : Flow<Int> = flow {
    emit(0)
    emit(1)
    emit(2)
    emit(3)
}
fun getNotDataDelayFlow() : Flow<Int> = flow {
    emit(0)
    delay(100)
    emit(1)
    delay(100)
    emit(2)
    delay(100)
    emit(3)
}
fun getFlow(): Flow<Int> = flow {
    emit(0)
    emit(4)
    emit(2)
    emit(6)
}
fun getDelayFlow(): Flow<Int> = flow {
    delay(2000)
    emit(0)
    delay(2000)
    emit(4)
    delay(2000)
    emit(2)
    delay(2000)
    emit(6)
}

 

과연 해당 flow 데이터를 사용하여 하기와 같이 값을 출력하려고 하면,

과연 결과는 어떻게 될까?

println("${flowData.filter { it > 3 }.first()}")

 

 

 

 


 

 

 

결과는 다음과 같다

  • getNotDataFlow(): java.util.NoSuchElementException 에러
  • getNotDataDelayFlow(): java.util.NoSuchElementException 에러
  • getFlow(): 4
  • getDelayFlow(): 어느 정도 시간이 흐른 후, 4

 

delay와는 상관없이 emit이 끝날 시점까지 조건에 맞는 데이터가 없을 경우 에러를 발생시킨다.

 

 

여기서 잠깐, 조건에 맞지 않는 데이터가 없을 경우 왜 에러를 발생시키는 것일까?

flow의 filter { }는 중간 연산자로, flow 데이터에 해당하는 조건만을 emit 해준다.

 


Flow.filter()

inline fun <T> Flow<T>.filter(crossinline predicate: suspend (T) -> Boolean): Flow<T>

주어진 술어(조건)와 일치하는 원래 흐름의 값만 포함하는 흐름을 반환합니다.

 

즉, 해당 flowData.filter { it > 3 }. first() 로직이 실행된다고 할 때,

first()로 steam이 실행되며,

flow에서  1, 2, 3, 4 ,5가 순서대로 emit 된다면,

filter { it > 3 }의 중간 연산자를 통해서 4, 5만 emit 되는 steam이 되는 것이다.

 

 

2025.07.06 - [Android/학습] - [Kotlin] Coroutine Flow란, 그리고 Flow, StateFlow, SharedFlow..

 

[Kotlin] Coroutine Flow란, 그리고 Flow, StateFlow, SharedFlow..

Coroutine Flow란?Flow는 Kotlin Coroutne 기반의 비동기 데이터 스트림 처리 도구이다. 데이터 스트림은 시간 흐름에 따라 연속적으로 발생하는 데이터를 순차적으로 처리하는 개념인데, Java의 Stream과 비

androidhelper.tistory.com

 

 

 

잠깐, 그렇다면 이 경우는 어떻까?

fun getNotDataRandomFlow() : Flow<Int> = flow {
    while (true) {
        val num = (0..3).random()
        emit(num)
    }
}
fun getRandomFlow() : Flow<Int> = flow {
    while (true) {
        val num = (0..10).random()
        emit(num)
    }
}
fun getRandomDelayFlow() : Flow<Int> = flow {
    while (true) {
        delay(3000)
        val num = (0..10).random()
        emit(num)
    }
}
  • getNotDataRandomFlow(): 계속 조건에 맞는 데이터를 emit 하는 Flow
  • getRandomFlow(): 계속 조건에 맞는 데이터가 emit 될 수도 있는 Flow
  • getRandomDelayFlow(): 시간 간격을 두고 계속 조건에 맞는 데이터가 emit 될 수도 있는 Flow

 

 

 

println("${flowData.filter { it > 3 }.first()}")

 

 

 


 

 

 

 

 

결과는 다음과 같다

  • getNotDataRandomFlow(): 끝나지 않음.
  • getRandomFlow(): 4 이상(3 초과)의 숫자를 받으면 해당 값을 출력하고 완료.
  • getRandomDelayFlow(): 어느 정도 시간이 흐른 후, 4 이상(3 초과)의 숫자를 받으면 해당 값을 출력하고 완료.

 

 

getNotDataRandomFlow()가 끝나지 않는 이유는 flow의 steam이 닫히지 않고 계속 현재 진행형이기 때문이다.

flow가 끝나지 않고 first()를 통해 첫 값을 받고 collect를 중지하는 것도 아니기 때문에 멈추지 않는 것이다.

(first가 받을 수 있는 데이터가 존재하지 않음 <- 조건에 따라 filter 되어, 해당하는 데이터가 없기 때문)

 

나머지 Flow는 동일한 이유로 Flow는 완료되지 않고 emit 되지만, first()를 통해 첫 값을 받고 collect가 중지된다.

 

 

 

 

 

Flow는 cold Steam이다.

collect(수집)할 때마다 새로운 Steam을 생성한다.

 


그렇다면 Hot Steam은 어떻까?

 

 

Hot Steam의 대표적인 ShareFlow와 State Flow 중, 일단 Share Flow를 집중적으로 살펴보자.

이전에 사용한 flowData를 hot Steam으로 바꿔보도록 하자.

val shareCoroutine = CoroutineScope(Dispatchers.Default)

val shareFlow = flowData.shareIn(
	scope = shareCoroutine,
	started = SharingStarted.WhileSubscribed(1000) //1초 동안 결과가 없을 경우 멈춤
)

 

2025.07.06 - [Android/학습] - [Kotlin] Coroutine Flow란, 그리고 Flow, StateFlow, SharedFlow..

 

[Kotlin] Coroutine Flow란, 그리고 Flow, StateFlow, SharedFlow..

Coroutine Flow란?Flow는 Kotlin Coroutne 기반의 비동기 데이터 스트림 처리 도구이다. 데이터 스트림은 시간 흐름에 따라 연속적으로 발생하는 데이터를 순차적으로 처리하는 개념인데, Java의 Stream과 비

androidhelper.tistory.com

 

 

다음 flow를 ShareFlow(Hot Steam)으로 변환하여 다시 결과를 확인해 볼 것이다.

기억이 안 난다면, 다시 위에서 함수를 확인해 보고 오자!

 

  • getNotDataFlow(): 조건에 맞는 데이터가 존재하지 않는 Flow
  • getNotDataDelayFlow(): 조건에 맞지 않는 데이터가 시간 간격을 두고 emit 되는 Flow
  • getFlow(): 조건에 맞는 데이터가 존재하는 Flow
  • getDelayFlow(): 조건에 맞는 데이터가 시간 간격을 두고 emit 되는 Flow
  • getNotDataRandomFlow(): 계속 조건에 맞는 데이터를 emit 하는 Flow
  • getRandomFlow(): 계속 조건에 맞는 데이터가 emit 될 수도 있는 Flow
  • getRandomDelayFlow(): 시간 간격을 두고 계속 조건에 맞는 데이터가 emit 될 수도 있는 Flow

 

 

println("${shareFlow.filter { it > 3 }.first()}")

 


 

 

  • getNotDataFlow(): 끝나지 않음. 계속 기다림.
  • getNotDataDelayFlow(): 끝나지 않음. 계속 기다림.
  • getFlow(): 4
  • getDelayFlow(): 어느 정도 시간이 흐른 후, 4
  • getNotDataRandomFlow(): 끝나지 않음. 계속 기다림.
  • getRandomFlow(): 4 이상(3 초과)의 숫자
  • getRandomDelayFlow(): 어느 정도 시간이 흐른 후, 4 이상(3 초과)의 숫자

 

 

Share Flow는 Flow와 다르게 어떤 Flow든 원하는 데이터가 emit 되지 않으면, 에러를 발생시키지 않고 기다리는 것을 확인할 수 있다.

원하는 결과 없으면 멈추지 않음.

 

 

 

그 이유는 Hot Steam은 Cold Steam과는 다르게 항상 값이 방출되기를 기대하기 때문에 완료되지 않기 때문이다.

그렇기 때문에 Hot Steam에서 NoSuchElementException 에러를 기대하기는 어렵다.

 

 

https://stackoverflow.com/questions/77984681/how-to-throw-exception-when-sharedflow-does-not-contain-item

 

How to throw Exception when SharedFlow does not contain item

I need to check that SharedFlow contain some item. And throw NoSuchElementException when this element is absent. I'm writed some code below, it work perfect when SharedFlow contain item. But if this

stackoverflow.com

 

 

 

그렇다면 멀리 돌아왔지만, 해당 로직의 flowData가 이벤트를 기대하는 ShareFlow라면,

var tmp: Int = -1
flowData.collect {
    if (it > 3) {
    	tmp = it
        cancel()
    }
}

다음과 같이 로직이 동일한 동작을 기대할 수 있을 것이다.

val tmp: Int = flowData.filter { it > 3 }.first()

 

 

근데, 사실 더 좋은 방법이 있다. ㅎㅎ

val tmp: Int = flowData.first { it > 3 }

 

 

 

 


 

Flow.first { }

suspend fun <T> Flow<T>.first(predicate: suspend (T) -> Boolean)

주어진 조건자 와 일치하는 흐름에서 방출된 첫 번째 요소를 반환하고 흐름의 수집을 취소하는 터미널 연산자입니다. 흐름에 조건자 와 일치하는 요소가 없으면 NoSuchElementException을 throw 합니다.

 

Flow.firstOrNull()

suspend fun <T> Flow<T>.firstOrNull(): T?

흐름에서 방출된 첫 번째 요소를 반환하고 흐름의 수집을 취소하는 터미널 연산자입니다. 흐름이 ​​비어 있으면 null 여부를 반환합니다.

 

Flow.firstOrNull { }

suspend fun <T> Flow<T>.firstOrNull(predicate: suspend (T) -> Boolean): T?

주어진 술어(조건)와 일치하는 흐름에서 방출된 첫 번째 요소를 반환하고 흐름의 수집을 취소하는 터미널 연산자입니다. 흐름에 술어(조건)와 일치하는 요소가 없는 경우 null를 반환합니다.

 

 

 

 

 

 

 

 

 

다들 즐코 하세요 ^^

 


추가적으로 알아볼만한 것

  • 내부 로직으로 알아보는 cold Steam, Hot Steam 개념
  • 내부 로직으로 first() 이해하기
  • flow의 완료와 cancel(), 중지 구분하기
  • shareFlow의 코루틴 취소되 때 first() 완료와 차이

 

 

 

참고 자료

더보기