Kt.Academy Kotlin Coroutines Deep Dive Summary 3부 — 코루틴 예외 처리 및 스코프 함수

GodDB
9 min readMar 23, 2022

--

본 게시글은 Kt.Academy의 Kotlin Coroutines DEEP DIVE의 요약본입니다.

목차

  1. 코루틴 원리 및 빌더
  2. 코루틴 컨텍스트 및 취소
  3. 코루틴 예외 처리 및 스코프 함수 — 현재 게시글
  4. 코루틴 디스패처 및 스코프 함수
  5. 코루틴 테스트
  6. Flow 빌더 및 기본 Operator
  7. Flow Operator

코루틴의 예외처리

출처 : https://kt.academy/article/cc-exception-handling

코루틴이 작동하는 방식에서 매우 중요한 부분은 Exception 처리입니다. catch 되지 않은 Exception가 발생하면 프로그램이 중단되는 것처럼, catch 되지 않은 Exception에 의해 코루틴이 중단 됩니다.

코루틴은 Exception 발생 시, 자체적으로 try-catch로 잡고 부모 Job에게 Exception을 전파합니다. 부모는 모든 자식 코루틴을 취소한 다음, 예외를 다시 부모에게 전파합니다.

그렇기 때문에, 최종 부모 코루틴까지 Exception이 전파 되도 프로그램은 종료되지 않습니다. (runBlocking은 예외. Job이 없기 때문에 프로그램이 종료된다.)

그렇기 때문에 코루틴 외부에서 try-catch로 잡는다고 해도 부모에게 Exception이 전파됩니다.

자식 코루틴에 의해 부모 코루틴도 종료되는 상황을 막기 위해선 SupervisorJob을 고려해야 합니다.

SupervisorJob

SupervisorJobJob 이랑 거의 동일합니다. 하나 다른점은, Exception을 부모에게 전파하지 않습니다.

supervisorScope

suspend 함수 내부에서 CoroutineScope를 가져올 수 있는 방법으론, withContext, coroutineScope, supervisorScope가 있습니다.

이 중 supervisorScope 는 자식 코루틴에게 SuperVisorJob을 적용시킵니다.

withContext()SuperVisorJob을 적용시키는 방법은 적용되지 않습니다.

await

앞서 코루틴에서 Exception 발생시, 부모 코루틴으로 전파를 막기 위해 SupervisorJob을 사용한다고 했습니다.

그렇다면 만약 이런 경우엔 어떨까요??

이 경우엔 Exception이 발생하여, str2의 결과가 나오지 않습니다.

이유는 str1async 블럭 내부의 Exception은 supervisorJob에 의해 부모 코루틴으로의 전파를 막지만, await()는 값을 전달받아야 하는데 Exception을 전달 받으므로, await()에서 Exception이 발생합니다.

이를 해결하기 위한 방법으로는 try-catch로 await()를 감싸서 해결할 수 있습니다.

CoroutineExceptionHandler

Exception이 발생했을 때의 시점을 알아야할 경우가 있을 수 있습니다. 그럴 때 CoroutineExceptionHandler를 이용해서 처리할 수 있습니다.

CoroutineExceptionHandler를 통해서 Exception이 전파되는 것은 막을 수 없습니다. 단지 Exception Listener입니다.

코루틴 Scope 함수

코루틴을 이용해서 병렬적으로 호출하는 방법은 무엇 일까요?

다음과 같이 했을 때는 병렬로 동작하지 않습니다.

병렬로 호출하기 위해서 새로운 코루틴을 생성해야 합니다.
하지만 명심해야 할 점은 구조적 동시성을 유지한 채로 코루틴을 생성해야 합니다.

그렇기에 우리는 coroutineScope()와 같은 코루틴 Scope 함수를 통해 자식 코루틴을 생성하고, 거기서 자식 코루틴을 생성 해야 합니다.

이렇게 처리하여 구조적 동시성을 유지한 상태에서 병렬로 요청할 수 있습니다.

또한, coroutineScope()launch()async()와는 조금 다른 흐름을 갖고 있습니다.

기본적으로 coroutineScope() 블럭 내부의 로직이 다 완료하기 전까지 하단의 코드를 실행 하지 않습니다.

코루틴 Scope 함수의 종류

suspend 함수 속에서 자식 코루틴을 생성하는 함수는 coroutineScope() 뿐만이 아닙니다. withContext(), supervisorScope(), withTimeOut()이 있습니다.

흔히 사람들은 코루틴 빌더 함수와 코루틴 scope 함수와 혼동을 합니다.

둘다 코루틴을 생성한다는 점에선 동일하지만, 용도와 사용법이 확연히 다릅니다. 당연히 코루틴 구현체도 다릅니다.

withContext

withContext()coroutineScope()와 유사합니다. 하지만 withContext()CoroutineContext를 변경 할 수 있습니다.

사실상 withContext(EmptyCoroutineContext)coroutineScope()는 동일하다고 볼 수 있습니다.

withContext()를 이용해서 CoroutineContext를 변경 할 수 있습니다.

하지만 보통 일반적으론 Dispatcher를 변경하기 위해 사용합니다.

coroutineScope()async(), await()와 유사한 실행 흐름을 갖고 있습니다. withContext(context)async(context), await() 와 유사합니다.
이 둘중 큰 차이점은 async()CoroutineScope가 필요하고, coroutineScope(), withContext()suspend 함수가 필요하다는 차이점이 존재합니다.
일반적으로 async() 직후 await()를 사용하는 패턴이라면 withContext()coroutineScope()를 사용하는 게 좋습니다.

supervisorScope

supervisorScope()coroutineScope()와 유사합니다. 외부 scope를 상속 받아, CoroutineScope를 생성 한 뒤, suspend block을 실행합니다.

차이점은 내부 코루틴 들이 Exception을 발생 시킬 때, 부모에게 Exception을 전달 하지 못하도록 처리 한다는 점에 있습니다

withTimeout

withTimeout()coroutineScope()와 유사합니다. 차이점은 withTimeout()은 해당 시간동안 내부 suspend block이 완료되지 않는다면 TimeoutCancellationException을 던지고 코루틴을 종료시킨다는 점입니다.

응용하기

프로필 화면을 사용자에게 보여주는 기능을 만든다고 가정해보겠습니다.

user의 이름, 친구들, 프로필 정보들을 병렬로 받아오고 다 완료 되었을 때, 서버에 프로필이 보여 졌다고 알려주는 로직 까지 입니다.

그렇다면 이렇게 만들어 볼 수 있을 것 입니다.

먼저 로딩을 보여주고, 앞서 말한 로직을 수행한 다음 로딩을 숨기는 것까지 진행됩니다.

하지만 여기서는 문제가 있습니다.
바로 25번째 줄 launch { repo.notifyProfileShown() } 입니다.

이 코드의 문제는 coroutineScope()의 특징 때문에 발생합니다.

coroutineScope()는 block 내에 모든 코루틴이 종료하기 전까지 종료되지 않으므로, launch { repo.notifyProfileShown() }이 정상적으로 완료될 때 까지 사용자에게 로딩이 보여지게 됩니다.

또한 launch { repo.notifyProfileShown() }에서 Exception이 발생하면, 부모 코루틴까지 영향이 발생됩니다.

우리가 원하는 것은, view.show() 후에는 병렬로 로딩이 제거되고, 서버에 프로필이 보여졌다는 정보를 보내는 것을 원합니다.

그렇게 하기 위해서는 launch { repo.notifyProfileShown() }를 자식 코루틴이 아닌 별도의 코루틴으로 만들어서 처리하면 병렬로 처리할 수 있습니다.

정리

코루틴 Exception 전파 흐름은 정리하자면 다음과 같습니다.

  • 코루틴 빌더로 생성한 코루틴은 Exception 발생 시 Job을 통해 부모 코루틴에게 전파한다.
  • 코루틴 스코프 함수로 생성한 코루틴은 Exception 발생 시, Exception을 throw한다.

그렇기 때문에 Exception 처리하는 로직이 달라집니다.

  1. 코루틴 빌더로 생성한 코루틴

내부에서 Exception 발생 시, 부모 코루틴에게 Exception을 전달하므로, 다음과 같이 대응해야 합니다.

  • 코루틴 내부에서 try-catch로 잡는다.
  • 코루틴의 Context를 SupervisorJob으로 생성하여, 부모 코루틴에게 Exception을 전달하지 않게 처리한다.

2. 스코프 함수로 생성한 코루틴

  • 코루틴 내부에서 try-catch로 잡는다.
  • 코루틴 외부에서 try-catch로 잡는다.
  • supervisorScope()을 사용하여 자식 코루틴에 SupervisorJob을 적용한다.

참고자료

--

--