suspendCoroutine, CallbackFlow

송훈기·2022년 8월 16일
0

Android

목록 보기
7/10

서론

안드로이드는 Callback의 연속이다.
당장 우리가 Default로 앱을 생성하게 되면 가장 먼저 만들어져 있는 것은 callback이다. ( override fun onCreate )
이러한 callback 패턴에는 제약이 많이 생긴다.
Retrofit을 사용한다고 예를 들어보자 Call객체를 활용해서 retrofit객체에 enqueue를 하게 되면 callback으로 우리는 이러한 형태의 로직을 작성하게 된다

object : Callback<Wrapper<Info, Characters>> {
                override fun onResponse(
                    call: Call<Wrapper<Info, Characters>>,
                    response: Response<Wrapper<Info, Characters>>
                ) {
                    // response가 성공이거나 실패이거나 등등의 내용의 Logic
                }

                override fun onFailure(call: Call<Wrapper<Info, Characters>>, t: Throwable) {
                   // 실패했을 경우의 Logic
                }

이러한 형태가 나쁘다는 것은 아니지만, 머리로 생각한 알고리즘을 코드로 풀어나가면서 부자연스러워진다. (개인적으로 난 그랬다. 이게 편하다면.. 이 다음의 내용은 읽지 않는 것을 추천한다.)
코드로 풀어나가기 힘들다는 게 어떤 의미인지를 생각해보면, callback은 한국말로 표현하자면 "우리 이런 행동을 하기로 약속하고, 넌 저 약속을 어떻게 실행할지 나에게 보여줘!"같은 느낌이다. 순전히 내 느낌이다.
그러나 세상의 요구사항은 그리 편하지 않다. 때론 callback이 아니라 값의 형태로 이를 받아내고 반환된 값으로 연속적으로 다른 행동을 할 수 있도록, 즉 "이 작업을 하고 그 결과 값을 나에게 줘!!"를 하고 싶어진다는 것이다.
이러한 표현방법을 할 수 있도록 Kotlin에서는 suspendCoroutine , CallbackFlow을 제공한다.
Coroutine이며, Flow의 기본 지식이 있어야 사용하는데 수월함이 있다.

이 글은 suspendCoroutine, CallbackFlow를 어떻게 활용할지에 대한 글이지 내부가 어떻게 생겼는지를 얘기하는 글은 아닙니다.
추천하는 블로그 아티클은 https://tourspace.tistory.com/442?category=797357 여기입니다.
굉장히 잘 작성되어 있습니다.

공통점

suspendCoroutine, callbackFlow 둘다 callback의 결과를 반환받을 수 있다.

차이점

간단하게 얘기하면
suspendCoroutine -> 단일 객체
callbackFlow -> Flow 스트림을 반환한다.

callback을 진행하면 익명의 객체가 특정 상태일 떄마다 callback을 실행한다. 그때 값을 단 한번만 받아서 사용한다면 suspendCoroutine, 계속해서 callback이 참조된 함수를 호출하며 값이 계속 변할거 같다. 즉, observe 패턴이 적용되는게 더 어울릴거 같을 떄 callbackFlow를 사용한다.

구현

suspendCoroutine

    Repository.kt
    
    suspend fun fetchCharacter(page: Int): Wrapper<Info, Characters>? {
        val result = suspendCancellableCoroutine<Wrapper<Info, Characters>?> { continuation ->
            val callbackImpl = object : Callback<Wrapper<Info, Characters>> {
                override fun onResponse(
                    call: Call<Wrapper<Info, Characters>>,
                    response: Response<Wrapper<Info, Characters>>
                ) {
                    val res = response.body()!!.apply {
                        isNetworkSuccessTag = "success"
                    }
                    continuation.resume(res) {}
                }

                override fun onFailure(call: Call<Wrapper<Info, Characters>>, t: Throwable) {
                    // Wrapper 클래스에 성공과 실패를 나눠줄 수 있는 Value를 둬서 그걸로 Trigger해도 괜찮다,
                    // 대신 더미로 만든다던지 Response의 값이 전부 nullable하게 된다는 단점이 존재한다.
                }
            }
            // Coroutine scope이 cancel 될 때 호출된다.
            continuation.invokeOnCancellation {
                // Thread - safe한 함수만 호출되어야 한다.

            }

            service.fetchCharacter(page).enqueue(callbackImpl)
        }

        return result
    }

callbackFlow

Repository.kt

suspend fun fetchCharacterWithFlow(page: Int): Flow<Wrapper<Info, Characters>> =
        callbackFlow<Wrapper<Info, Characters>> {
            val callbackImpl = object : Callback<Wrapper<Info, Characters>> {
                override fun onResponse(
                    call: Call<Wrapper<Info, Characters>>,
                    response: Response<Wrapper<Info, Characters>>
                ) {
                    trySend(response.body()!!.apply { isNetworkSuccessTag = "success" })
                }

                override fun onFailure(call: Call<Wrapper<Info, Characters>>, t: Throwable) {
                    close()
                }
            }

            service.fetchCharacter(page).enqueue(callbackImpl)

            // coroutineScope 이 cancel 또는 close 될때 호출

            // ProducerScope block의 코드의 실행이 완료되고 나서 바로 종료되는 것을 막는 코드.
            // addListener로 특정 이벤트를 관찰하는 겨웅에는 callback이 호출되는 걸 지속적으로 관찰해야 하기 때문에 api를 사용해 지속적으로 callback을 전달 받을 수 있도록 flow를 유지한다.

            // awaitClose는 flow가 cancel되거나 close 될 떄(channel close()가 명시적으로 호출될 때) 해당 블록을 호출
            // 해당 block안에서는 resource를 해제하는 코드가 들어가야한다.
            awaitClose {
                cancel()
            }
        }
profile
안녕하세요 송훈기입니다.

0개의 댓글