๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
Programando/Android

[Android/Kotlin] Coroutine ์˜ˆ์™ธ ์ฒ˜๋ฆฌ

๐Ÿคฆ‍โ™€๏ธ ๋ฌธ์ œ ์ƒํ™ฉ

API์˜ ์ฃผ์†Œ์— ์˜ค๋ฅ˜๊ฐ€ ์ƒ๊ธฐ๊ฑฐ๋‚˜ ํ˜น์€ ๋‹ค๋ฅธ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ์— ์•ฑ์ด ๋น„์ •์ƒ์ ์œผ๋กœ ์ข…๋ฃŒ๋˜๋Š” ํ˜„์ƒ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด Coroutine์— ์˜ˆ์™ธ์ฒ˜๋ฆฌ ์ฝ”๋“œ๊ฐ€ ํ•„์š”ํ–ˆ์Šต๋‹ˆ๋‹ค.

    // Coroutines Error ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ์ฝ”๋“œ
    override suspend fun getApi1FromApi2(): CheckAppVersionResponse {
        val deferred = CoroutineScope(Dispatchers.IO).async {
            val checkAppType = async {
                getCheckAppType().appType
            }

            getCheckAppVersion(checkAppType.await())
        }

        return deferred.await()
    }

 

๐Ÿงจ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ์ด์œ  (์ถ”์ •)

  • launch ํƒ€์ž…์˜ ๋นŒ๋”๋กœ ์ƒ์„ฑ๋œ ์ฝ”๋ฃจํ‹ด์ด ๋ฃจํŠธ ์ฝ”๋ฃจํ‹ด์ธ ๊ฒฝ์šฐ, ์ฝ”๋ฃจํ‹ด ๊ณ„์ธต ๊ตฌ์กฐ ๋‚ด์—์„œ ๋ฐœ์ƒํ•œ ์˜ˆ์™ธ๋Š” ๋ฃจํŠธ ์ฝ”๋ฃจํ‹ด์˜ ์˜ˆ์™ธ ํ•ธ๋“ค๋Ÿฌ์— ์˜ํ•ด ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค.
    • Exception์„ ์™ธ๋ถ€๋กœ ์ „ํŒŒ(propagation) ์‹œํ‚จ๋‹ค.
  • async ํƒ€์ž…์˜ ๋นŒ๋”๋กœ ์ƒ์„ฑ๋œ ์ฝ”๋ฃจํ‹ด์ด ๋ฃจํŠธ ์ฝ”๋ฃจํ‹ด์ธ ๊ฒฝ์šฐ, ์ฝ”๋ฃจํ‹ด ๊ณ„์ธต ๊ตฌ์กฐ ๋‚ด์—์„œ ๋ฐœ์ƒํ•œ ์˜ˆ์™ธ๋Š” ํ˜ธ์ถœ์ž์—๊ฒŒ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ๋งก๊ธฐ๋ฉฐ ๋ฃจํŠธ ์ฝ”๋ฃจํ‹ด์˜ ์˜ˆ์™ธ ํ•ธ๋“ค๋Ÿฌ๋Š” ๋™์ž‘ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. (ํ˜ธ์ถœ์ž๊ฐ€ await(), receive() ๋“ฑ์˜ ๋ฐ์ดํ„ฐ ์ˆ˜์‹  ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•จ)
    • Exception์„ ๋…ธ์ถœ(exposing) ์‹œํ‚จ๋‹ค.

 

async๋กœ ์ƒ์„ฑํ•œ ์ฝ”๋ฃจํ‹ด์„ async์—์„œ await()ํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ ์ฝ”๋ฃจํ‹ด์—์„œ ์ฒ˜๋ฆฌํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์•ฑ์ด ๋น„์ •์ƒ์ ์œผ๋กœ ์ข…๋ฃŒ๋˜๋Š” ํ˜„์ƒ์€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    /**
     * MainViewModel.kt
     */
    private fun coroutinesErrorHandling() {
        viewModelScope.async {
            Log.d(TAG, "${comApiRepository.getApi1FromApi2()}")
        }
    }

 

ํ•˜์ง€๋งŒ ๋งŒ์•ฝ async๋กœ ์ƒ์„ฑํ•œ ์ฝ”๋ฃจํ‹ด์„ ์˜ˆ์™ธ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ๋‹ฌ์ง€ ์•Š์€ launch ๋ธ”๋Ÿญ์—์„œ await()ํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ์ „ํŒŒ๋˜๋ฉด์„œ ์•ฑ์ด ๋น„์ •์ƒ์ ์œผ๋กœ ์ข…๋ฃŒ๋˜๋Š” ํ˜„์ƒ์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

    /**
     * MainViewModel.kt
     */
    private fun coroutinesErrorHandling() {
        viewModelScope.launch {
            Log.d(TAG, "${comApiRepository.getApi1FromApi2()}")
        }
    }
--------- beginning of crash
E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.rit.dynamicurlapi, PID: 6508
    kotlin.KotlinNullPointerException: Response from com.rit.data.remote.service.ApiService.checkAppType was null but response body type was declared as non-null
        at retrofit2.KotlinExtensions$await$2$2.onResponse(KotlinExtensions.kt:43)
        at retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:161)
        at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:535)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:764)
        Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@f3485f9, Dispatchers.Main.immediate]

 

๋ฐ˜์‘ํ˜•

 

๐Ÿ’Š ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

1. CoroutineExceptionHandler ์‚ฌ์šฉ

    /**
     * MainViewModel.kt
     */
    private val _state = MutableLiveData<State>()
    val state: LiveData<State>
        get() = _state

    // CoroutineExceptionHandler ์ƒ์„ฑ
    protected val coroutineExceptionHandler = CoroutineExceptionHandler { _, exception ->
        Log.e("CoroutineExceptionHandler", "${exception.message}")

        when(exception) {
            is SocketException -> _state.postValue(State.BAD_INTERNET)
            is HttpException -> _state.postValue(State.PARSE_ERROR)
            is UnknownHostException -> _state.postValue(State.WRONG_CONNECTION)
            else -> _state.postValue(State.FAIL)
        }
    }

    private fun getData() {
        // ๋ฃจํŠธ ์ฝ”๋ฃจํ‹ด์— CoroutineExceptionHandler ๋‹ฌ๊ธฐ
        viewModelScope.launch(coroutineExceptionHandler) {
            Log.d(TAG, "${comApiRepository.getApi1FromApi2()}")
        }
    }

 

2. runCatching ๊ตฌ๋ฌธ

2-1. ๋ฃจํŠธ ์ฝ”๋ฃจํ‹ด์ด async

    /**
     * MainViewModel.kt
     */
    private fun coroutinesErrorHandling() {
        viewModelScope.async {
            Log.d(TAG, "${comApiRepository.getApi1FromApi2ErrorHandlingResult()}")
        }
    }
    /**
     * RepositoryImpl.kt
     */
    override suspend fun getApi1FromApi2ErrorHandlingResult(): CheckAppVersionResponse? {
        val checkAppType = CoroutineScope(Dispatchers.IO).async { getCheckAppType().appType }
        val checkAppVersion = CoroutineScope(Dispatchers.IO).async { getCheckAppVersion(checkAppType.await()) }

        return kotlin.runCatching {
            checkAppVersion.await()
        }.onSuccess {
            Log.d(this::class.java.name, "$it")
        }.onFailure {
            Log.e(this::class.java.name, "${it.message}")
        }.getOrThrow()
    }

 

2-2. ๋ฃจํŠธ ์ฝ”๋ฃจํ‹ด์ด launch

    /**
     * MainViewModel.kt
     */
    private fun coroutinesErrorHandling() {
        viewModelScope.launch {
            Log.d(TAG, "${comApiRepository.getApi1FromApi2ErrorHandlingResult()}")
        }
    }
    /**
     * RepositoryImpl.kt
     */
    override suspend fun getApi1FromApi2ErrorHandlingResult(): CheckAppVersionResponse? {
        val checkAppType = CoroutineScope(Dispatchers.IO).async { getCheckAppType().appType }
        val checkAppVersion = CoroutineScope(Dispatchers.IO).async { getCheckAppVersion(checkAppType.await()) }

        return kotlin.runCatching {
            checkAppVersion.await()
        }.onSuccess {
            Log.d("Repository", "$it")
        }.onFailure {
            Log.e("Repository", it.message.toString())
        }.getOrNull()
    }

 

2-3. ๋ฃจํŠธ ์ฝ”๋ฃจํ‹ด์—์„œ runCatching

    /**
     * MainViewModel.kt
     */
    private fun coroutinesErrorHandling() {
        viewModelScope.launch {
            kotlin.runCatching {
                comApiRepository.getApi1FromApi2()
            }.onSuccess {
                Log.d(TAG, "$it")
            }.onFailure {
                Log.e(TAG, "${it.message}")
            }
        }
    }

 

๊ทธ ์™ธ

์ž๋ฃŒ ์ฐธ์กฐ

https://june0122.tistory.com/20

https://medium.com/naverfinancial/์ฝ”๋ฃจํ‹ด-์˜ˆ์™ธ-๋‹ค๋ฃจ๊ธฐ-acb5b91dad0a

https://uchun.dev/runCatching์„-์ด์šฉํ•œ-kotlin์—์„œ-exception์ฒ˜๋ฆฌ-๋ฐฉ๋ฒ•/

๋ฐ˜์‘ํ˜•