본문 바로가기
도서/코틀린 코루틴

플로우란 무엇인가?

by 안스 인민군 2024. 3. 24.

플로우란 비동기적으로 처리해야할 값들의 데이터스트림이다.

 

즉 플로우 자체는 데이터를 모으고 그 끝에는 방출하는 역할을 한다.

Flow 의 함수중 collect 가 그 역할을 한다. (== forEach )

실제로 Flow 의 내부구현을 봐보면 유일한 멤버 함수인 collect 을 제외하고 다른 함수들은 확장 함수로 정의되어 있다.

 public interface Flow<out T> {
        public suspend fun collect(collector: FlowCollector<T>)
}

플로우와 값들을 나타내는 다른 방법들과 비교

여러개의 값들을 반환하고 싶을 때 List 같은 컬렉션을 사용할 수 있다.

아래 예제를 살펴보자.

fun getList() = List(3){
    Thread.sleep(1000)
    "list item $it"
}

fun main(){
    val list = getList() // 3초 기다려야함.
    println("start")
    list.forEach(::println)
}

// 
(3초 후)
start
list item 0
list item 1
list item 2

List 는 할당할 때 계산을 완료하기 때문에 값이 채워질 때 까지 기다려야 한다.

따라서 위의 예제에서 3초를 기다리고 출력하는 모습을 볼 수 있다.

 

다른 컬렉션 중 sequence 같은 경우 이런 연산이 필요할 때마다 계산하기 때문에 조금 더 용이하게 사용할 수 있다.

fun getSequence() = sequence<String> {
    repeat(3) {
        Thread.sleep(1000)
        yield("sequence item : $it")
    }
}

fun main() {
    val list = getSequence()
    println("start")
    list.forEach(::println) // loop 돌 때 delay 가 됨
}
// 
start
1초 후
sequence item : 0
1초 후
sequence item : 1
1초 후
sequence item : 2

sequence 와 Flow 는 동일한 역할을 한다. 왜 sequence 가 아닌 Flow 를 사용하는 걸까?

sequence 의 최종연산은 때 중단함수 내부에서 사용하지 않으므로 HTTP 통신같은 경우에 사용할 때 적합하지 않다.

// 잘못된 코드
suspend fun getUser(
    api: UserApi,
) = sequence<User> {
    val user = api.getAPI() // 중단함수와 같이 사용할 수 없음

    yield(user)
}

위의 예제와 같은 경우에 Flow 를 사용해야 한다.

플로우의 특징

플로우의 최종연산(collect)에서는 코루틴을 중단시키고, 구조화된 동시성, 예외처리 등 기본적인 기능들을 가지고 있다.

아래 예제를 살펴보자.

fun getFlow() = flow {
    repeat(3) {
        delay(1000)

        emit("it in ${currentCoroutineContext()[CoroutineName]?.name}")
    }
}


suspend fun main() {
    val flow = getFlow()

    withContext(CoroutineName("Name")) {
        val job = launch {
            flow.collect(::println)
        }
        launch {
            delay(2100)
            println("finish")
            job.cancel()
        }
    }
}
//
it in Name
it in Name
finish

Flow 는 collect 할 때 부모-자식 관계가 성립이 된다.

따라서 부모가 취소될때 자식도 취소가 되므로 emit 이 2번만 된 것을 확인할 수 있다.

플로우 명명법

 

플로우는 몇가지 요소로 구성이 된다.

  • 플로우는 어딘가 에서 실행되어야 한다.
    • 어딘가 는 플로우 빌더, 다른 객체의 변환, 헬퍼 함수 등을 뜻한다.
  • 플로우의 마지막 연산은 최종연산 이라고 부른다.
    • 최종연산 에서 유일하게 스코프를 필요로 하므로 중요하다.
  • 시작 연산과 최종 연산 사이의 플로우를 변경할 수 있는 중간 연산 을 가질 수 있다.
suspend fun main() {
    flow { emit("123") }// 시작 연산, 플로우 빌더
        .onEach(::println) // 중간연산
        .onStart(::println) // 중간연산
        .onCompletion { emit("finish") } // 중간연산
        .collect() // 최종 연산
}

 

개인적으로 플로우의 기능중 중간연산이 가장 강력한 기능이라고 생각한다.