[Coroutine] callback API를 Flow로 변환해보기

라이브러리나 sdk에서 callback 형태로 받는 API를 Flow로 받고 싶을 땐 어떻게 해야 할까요?!

callbackFlow, suspencoroutine 등을 이용하여 Flow로 받을 수 있습니다.

이번에는 callbackFlow에 대해 공부해보려고 합니다.


1. callbackFlow란

public fun <T> callbackFlow(@BuilderInference block: suspend ProducerScope<T>.() -> Unit)
	: Flow<T> = CallbackFlowBuilder(block)

callbackFlow는 callback을 Flow로 변환할 수 있는 Flow Builder입니다.

공식 문서에 따르면, ProducerScope를 통해 빌더 블록의 코드에 전달된 SendChannel에 요소를 보내는 "cold Flow"의 인스턴스를 생성합니다. 이를 통해 요소는 다른 컨텍스트나 동시에 실행 중인 코드에서 생성될 수 있습니다. 

라고 적혀있는 것을 확인할 수 있습니다. 무슨 말인지 잘 이해가 되지 않으니 코드를 보며 다시 살펴보도록 하겠습니다.

 

callbackFlow는 ProducerScope을 파라미터로 전달받고, 해당 block을 callbackFlowBuilder()에게 전달합니다. callbackFlowBuilder()를 확인해 보면 채널이 사용되는 것을 알 수 있고 ProducerScope는  채널에 연결됩니다. ProducerScope를 사용하여 데이터를 채널로 보내거나 채널에서 데이터를 수신할  있게 됩니다. 즉, callbackFlowBuilder를 통해 channel을 Flow로 변환시킵니다.

 

 

2. callbackFlow 사용해 보기

프로젝트를 진행 중 카카오 로그인 성공과 동시에 fcm token을 얻어와야 했고 기존 sdk 콜백 함수를 사용했을 경우 값을 얻어오는 과정에서 타이밍 이슈가 발생하여 로그인 state와 fcm token을 얻어오는 state를 flow로 변환하는 작업이 필요했었습니다. 이 경험을 토대로 Firebase에서 FcmToken을 가져오는 코드를 Flow로 변환해 보도록 하겠습니다.

FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task ->
    if (!task.isSuccessful) {
        Log.w(TAG, "Fetching FCM registration token failed", task.exception)
        return@OnCompleteListener
    }

    // Get new FCM registration token
    val token = task.result

    // Log and toast
    val msg = getString(R.string.msg_token_fmt, token)
    Log.d(TAG, msg)
    Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
})

해당 코드는 공식문서에 나와있는 addOnCompleteListener 메서드를 통해 토큰 task를 가져올 수 있었습니다. 이제 flow로 변환해 보도록 하겠습니다. 

 

fun getFcmToken(): Flow<String> = callbackFlow {
        val onCompleteListener = OnCompleteListener<String> { task ->
            if (task.isSuccessful) {
                val token = task.result
                if (token != null) {
                    Log.d(TAG, "getFcmToken: $token")
                    trySend(token)
                }
            } else {
                close(task.exception ?: Exception("Failed to fetch FCM registration token"))
            }
        }

        FirebaseMessaging.getInstance().token.addOnCompleteListener(onCompleteListener)
        awaitClose()
 }

위 코드를 살펴보면 trySend, awaitClose() 등을 확인할 수 있는데요, 하나씩 살펴보도록 하겠습니다.

 

trySend

- trySend는 현재 상태나 결과값을 전달할 수 있으며, 현재 코드에서는 Flow <String>을 반환하기 때문에 토큰값을 전달합니다. 

- 내부적으로 callbackFlow는 개념상 Blocking 와 매우 유사한 채널을 사용합니다. 채널은 버퍼링 가능한 요소의 최대 수인 용량으로 구성됩니다. callbackFlow에서 생성된 채널의 기본 용량은 요소 64개입니다. 전체 채널에 새 요소를 추가하려는 경우 send는 새 요소를 위한 공간이 생길 때까지 생산자를 정지하는 반면, trySend는 채널에 요소를 추가하지 않고 즉시 false를 반환합니다.

 

awaitClose

- 플로우 수집이 취소될 때 또는 콜백 기반 API가 SendChannel.close를 수동으로 호출할 때 호출되며, 일반적으로 완료 후 리소스를 정리하는 데 사용됩니다. awaitClose를 사용하지 않으면 콜백이 플로우 수집이 이미 완료된 경우에도 계속 실행될 수 있습니다. 

 

 

 

마무리

지금까지 callback API를 Flow로 변환하는 방식에 대해 알아보았습니다. Android 개발을 하다 보면 외부 SDK나 라이브러리의 기본 callback 패턴은 제약이 있을 수 있습니다. 상황에 맞게 Flow로 변환하여 사용하는 방법도 알아두면 좋을 것 같습니다!

다음은 suspendCoroutine과 suspendCancellableCoroutine에 대해 알아보도록 하겠습니다.

 

 

 

 

 

참고

https://developer.android.com/kotlin/flow

https://medium.com/@apfhdznzl/callback%EC%9D%84-flow%EB%A1%9C-%EB%B3%80%EA%B2%BD-4030cde1310

https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/callback-flow.html