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

플로우의 실제 구현

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

플로우가 실제 어떻게 내부적으로 확인해 보자.

이를 하기위해 먼저 람다식을 만들어 실행하는 코드를 작성해 보자.

fun main() {
    val f: () -> Unit = {
        print("1")
        print("2")
        print("3")
        println()
    }

    f()
    f()
    f()
}
//
123
123
123

결과는 누구나 예상할 수 있듯이 123 이 출력되고 있는 것을 확인할 수 있다.

흥미를 더 돋우기 위해 중단함수를 추가해 보자.

suspend fun main() {
    val f: suspend () -> Unit = {
        print("1")
        delay(1000)
        print("2")
        delay(1000)
        print("3")
        println()
    }

    f()
    f()
}
//
123 // 2초
123 // 2초

람다식은 순차적으로 호출되기 때문에, 이전 호출이 완료될 때까지 같은 람다식을 호출할 수 없다.

 

람다식은 함수를 나타낼 수 있는 파라미터를 가질 수 있다.

이 파라미터를 emit 이라고 해보자.

람다식 f 를 호출 할 때 emit 으로 또다른 람다식을 명시해야 한다.

suspend fun main() {
    val f: suspend ((Int) -> Unit) -> Unit = { emit ->
        emit(1)
        emit(2)
        emit(3)
    }

   f { print(it) }
   f { print(it) }
}
// 123123

함수명이 많이 복잡해진 상태이므로, emit 함수 메서드를 가진 FlowCollector 를 만들어 사용해보자.

fun interface FlowCollector {
    suspend fun emit(value: Int)
}

suspend fun main() {
    val f: suspend (FlowCollector)  -> Unit = {
        it.emit(1)
        it.emit(2)
        it.emit(3)
    }

    f { print(it) }
    f { print(it) }
}
// 123123

위의 코드에서 더 나아가 it 에서 emit 을 호출하는 것은 깔끔하지 않으므로 FlowCollector 를 리시버로 만들어 사용해보자.

fun interface FlowCollector {
    suspend fun emit(value: Int)
}

suspend fun main() {
    val f: suspend FlowCollector.() -> Unit = { // this : FlowCollector
        emit(1)
        emit(2)
        emit(3)
    }

    f { print(it) }
    f { print(it) }
}
// 123123

람다식을 전달하는 것 보다 인터페이스로 구현된 객체를 만드는 것이 낫다.

이 때 인터페이스 이름을 Flow 로 하고 인터페이스의 정의는 객체 표현식으로 매핑하면 된다.

fun interface FlowCollector {
    suspend fun emit(value: Int)
}

interface Flow {
    suspend fun collect(collector: FlowCollector)
}

suspend fun main() {
    val builder: suspend FlowCollector.() -> Unit = {
        emit(1)
        emit(2)
        emit(3)
    }

    val flow: Flow = object : Flow {
        override suspend fun collect(collector: FlowCollector) {
            collector.builder()
        }
    }

    flow.collect(::println) //123
    flow.collect(::println) // 123
}
// 123123

이 때 flow 생성을 간단하기 위해 flow 빌더를 정의해보자.

fun interface FlowCollector {
    suspend fun emit(value: Int)
}

interface Flow {
    suspend fun collect(collector: FlowCollector)
}

fun flow(
    builder: suspend FlowCollector.() -> Unit,
) = object : Flow {
    override suspend fun collect(collector: FlowCollector) {
        collector.builder()
    }
}

suspend fun main() {
    val flow: Flow = flow {
        emit(1)
        emit(2)
        emit(3)
    }

    flow.collect(::print) // 123
    flow.collect(::print) // 123
}
//
123123

마지막으로 Int 타입 뿐만 아니라 모든 타입으로 사용할 수 있도록 제너틱 타입으로 변경해보자.

fun interface FlowCollector<T> {
    suspend fun emit(value: T)
}

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

fun <T> flow(
    builder: suspend FlowCollector<T>.() -> Unit,
) = object : Flow<T> {
    override suspend fun collect(collector: FlowCollector<T>) {
        collector.builder()
    }
}

suspend fun main() {
    val flow: Flow<Int> = flow {
        emit(1)
        emit(2)
        emit(3)
    }

    flow.collect(::print) // 123
    flow.collect(::print) // 123
}
//
123123

위에서 만든 Flow을 구현한 방식은 실제 Flow 가 개발되어 있는 모습과 거의 흡사하다.

collect() 를 호출하면, flow 빌더를 호출할 때 넣는 람다식이 호출된다.

빌더의 람다식이 emit 을 호출하면 collect 가 호출되었을 때 명시된 람다식이 호출된다.

'도서 > 코틀린 코루틴' 카테고리의 다른 글

플로우 생명주기 함수  (0) 2024.03.24
플로우 만들기  (0) 2024.03.24
핫 데이터 소스와 콜드 데이터 소스  (0) 2024.03.24
플로우란 무엇인가?  (0) 2024.03.24
3장 채널과 플로우 - 채널  (0) 2024.03.08