마스터Q&A 안드로이드는 안드로이드 개발자들의 질문과 답변을 위한 지식 커뮤니티 사이트입니다. 안드로이드펍에서 운영하고 있습니다. [사용법, 운영진]

코루틴 launch 안에서 exception

0 추천
코루틴안에서 exception을 어떻게 잡아야될까요..

flow + api 호출하는 부분은 .collect 이전에 .catch를 호출해서 잡히는데

api호출도 아니고 flow도 아니고 .catch도 안써집니다

try-catch로도 안먹히구요 coroutineexceptionhandler에서도 안잡힙니다

lifecyclescope.launch {

   adapter.launch {
       lifecyclescope.launch {

          ...

    }

  }

}

... 부분이 인터넷 끊었을때 마지막으로 호출되고 앱이 크래시 나는부분인데 저기 익셉션이 안잡히네용 어떻게 해야될까요
수원통학러 (3,570 포인트) 님이 2022년 1월 3일 질문

1개의 답변

0 추천

코루틴의 exception 처리와 cancellation에 관해서는 반드시 GoogleIIO 관련 영상(https://www.youtube.com/watch?v=w0kfnydnFWI&t=30s) 이나Jetbrain의 개발자 가이드(https://kotlinlang.org/docs/exception-handling.html)를 확인하세요. 코루틴에서 이 두가지 부분이 가장 난해한 주제입니다. 왜냐하면 직관적이지 않기 때문에 자칫하면 버그를 만들 수있기 때문입니다.

코루틴의 예외 처리 원칙은 이렇습니다. 

try catch를 사용해야 할 때는 가장 안쪽의 scope에서 해준다. Coroutine builder(예: launch, runBlocking, async)를 사용할 때 내부적으로 Coroutine scope이 만들어 집니다. 그리고 이 안에서 발생된 exception은 기본적으로 parent scope이 존재할 경우 parent scope으로 전달됩니다. 이걸 Job hiearachy라고 부르는데, 코루틴을 사용할 때 잘 이애해야 하는 부분 중의 하나입니다. 이 때는 아무리  try catch로 예외를 잡으려고 해도 잡히지 않습니다. Coroutine의 ExceptionHandler를 사용하거나 SupervisorScope를 사용하면 예외를 잡을 수 있습니다.

따라서, 가능하면 불필요한 builder를 호출하지 않도록 하는 것 이 좋습니다. 그리고 좀 더 골치가 덜 아픈 접근 방법은, suspend function이 예외를 던지지 않도록 하는 겁니다. 예를 들면 getUsers라는 API을 호출하는 코드가 있다고 하면, 아래처럼 예외를 내부적으로 에러와 성공을 나타내는 sealed class로 변환하여 리턴하는 겁니다.

sealed class Result<T> {
   data class Error(val e: Exception): Result<Nothing>()
   data class Success(val data: T): Result<T>()
}

suspend fun getUsers(): Result<List<User>> {
   return try {
       val users = api.getUsers()
       Result.Success(users)
   } catch (e: Exception) {
       Result.Error(e)
   }
}

 

끝으로, 님의 코드에서,

lifecyclescope.launch {  // 첫번째 CoroutineScope

   adapter.launch { // 두번째 CoroutineScope. lifecyclescope의 CoroutineScope과 다름.
        lifecyclescope.launch { //  세번째 CoroutineScope. 위의 adapter와 lifecyclescope의 CorountineScope과 다름. 

          ...

        }

    }

}

adapter.launch가 뭔가 이상하구요, lifecycleScope.launch는 왜 다시 한번 호출이 되고 있는지 의아하네요. 위처럼 하시면 별개의 CoroutineScope이 세개가 독립적으로 실행되게 되기 때문에 부모-자식의 관계가 성립되지 않고 adapter.launch나 제일 안쪽의 lifecyclescope.launch는 순차적으로 실행되리라는 보장이 없어요. 다른 CoroutineScope이 종료되는 걸 기다리지 않고, 먼저 실행되는 루틴이 먼저 종료될 겁니다.그리고 같은 lifecyclescope를 연달아 사용하고 있기 때문에 그로 인한 문제점이 어떤 것이 있을지 가늠하기가 직관적인지 않습니다. launch를 잘 보시면 block 람다가 CoroutineScope을 사용하는 걸 알 수 있어요.

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext, 
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit // <---
): Job {
   ...
}

따라서, 아래처럼 같은 CoroutineScope을 사용하셔야 해요. 물론 일부로 그렇게 의도하신거면 상관없지만요.

lifecyclescope.launch { 
     
     launch { // this.launch와 동일. lifecycle.launch에서 사용하는 동일한 CoroutineScope를 사용

     }
}

 

spark (227,470 포인트) 님이 2022년 1월 3일 답변
spark님이 2022년 1월 4일 수정
거의 대부분의 앱에서는 액티비티, 프레그먼트는 LifecycleScope, 뷰모델은 ViewModelScope만 사용하시면 충분합니다.
전개발자분이 이렇게 해서 정확한 의도는 모르겠으나 launch안에 있는 launch에서 하는건 프로그레스    suspend fun loadingWithSuspend(isLoading: Boolean) {
        if(activity?.isDestroyed == true || activity?.isFinishing == true) return
        withContext(Dispatchers.Main) {
            if(isLoading) loadingDialog.show()
            else loadingDialog.dismiss()
        }
    }
으로 해서 전역적으로 다 사용중인 형태입니다 그외 다른 로직은 가장 바깥 launch안에서 사용중입니다 api call은 없구요 네트워크를 끊었을때 디버깅 포인트 다찍어놓고 돌려보면 지금 위에 저코드를 호출하는 메소드를 마지막으로 호출 후에 죽습니다 여기를 어떻게 catch를 해야될까요?
제가 앱에서 해당 코드가 어떻게 사용하는지와 어떤 에러가 나는지에 대한 이해가 없기 때문에, 추가적인 말씀은 드리기가 힘드네요.
네트워크를 끊을시 socket exception으로 나옵니다 답변주신거처럼 안에 있는 lifecyclescope.launch를 launch로 수정 exceptionhandler를 최상위에 붙여보고, 거기에 안에 launch에 try catch도 추가해봤는데 안잡히네용
가장 심플하지만 보기 좋지않은 해결방법은 아래처럼  ExceptionHandler lauch마다 설정하는 건데, 이렇게 하고 싶지는 않으실 겁니다.

firstScope.launch(exceptionHandler) {
     secondScope.launch(exceptionHandler) {
            thirdScope.launch(exceptionHandler) {

            }
     }
}

가능하다면 lauch builder가 하나만 실행될 수 있는지 잘 체크해 보시고 하나만 사용할 수 있다면 하나만 사용하시고  exceptionHandler를 다세요. 하지만 이 방법도 여전히 바람직한 방향은 아닙니다. 위의 답글에서 언급했듯이, 네트워크 레이어에서  exception 를 잘 감싸서 처리하는게 더 좋습니다.
네트워크 관련 코드가 다른 CoroutineScope를 사용하고 있다면, 그렇게 될 수 있습니다. try catch로 예외를 잡으려면 SupervisorJob을 사용하는 것이 좋구요.
네트워크 관련 코드와 뷰에서 어떻게 사용되고 있는지 보여주시면 답을 드리기가 좀 쉬울 것 같아요.
현재 문제되고 있는 코드와 같은 곳에 있는 api호출하는 부분은 .catch로 잡아놓은 상태입니다만 launch안에 launch는 단순히 settext, visible만 처리하고 있습니다 근데 왜 저기서 죽고있는건지..
그런 경우는 launch안에 launch를 사용하실 필요가 없어요. Thread를 백그라운드에서 메인으로 바꾸어야 한다면 withContext(Dispatcher.Main.Immediate)를 쓰시면 그만입니다.
따라서 님의 코드는
lifecycleScope.launch {

      withContext(Dispatcher.Main.immediate) {
          //메인쓰레드에서 뷰에 대한 작업을 여기서 처리
      }
}

이런 식으로 되어야 할 것 같은데, 액티비티나 프레그먼트에서는 메인쓰레드만 사용하면 되는데, 쓰레드 스위칭이 필요한지 이해가 되지 않네요. lifecycleScope.launch하나만으로도 충분할 것 같은데요.
...