Image
Coroutines/Coroutine Basics

[Coroutine] 11. Coroutine CoroutineContext를 다루는 방법 : Coroutine Dispatcher과 ExceptionHandler을 CoroutineContext를 이용해 관리하기

CoroutineContext

앞서 우리는 다음의 내용들을 배웠다.

  • Dispatcher: 코루틴이 실행될 스레드 풀을 잡고 있는 관리자
  • CoroutineExceptionHandler: 코루틴에서 Exception이 생겼을 때의 처리기

그런데 이 두 가지 요소는 CoroutineContext가 들어가야 할 자리에 그대로 들어갈 수 있다.

  • <그림1>에서는 Dispatcher가 CoroutineContext 자리에 들어간다.
  • <그림2>에서는 CoroutineExceptionHandler가 CoroutineContext자리에 들어간다.

그림1. Dispatcher가 CoroutineContext에 들어간다.
그림2. CoroutineExceptionHandler가 CoroutineContext 자리에 들어간다.

이것이 가능한 이유는 각각이 CoroutineContext를 확장하는 인터페이스의 구현체이기 때문이다.

 

*CoroutineDispatcher의 내부 살펴보기

더보기
public abstract class CoroutineDispatcher : .. , ContinuationInterceptor { .. }

public interface ContinuationInterceptor : CoroutineContext.Element { .. }

public interface CoroutineContext {
    public interface Element : CoroutineContext { .. }
}

CoroutineDispatcher은 ContinuationInterceptor 인터페이스를 구현하는 추상 클래스이며, ContinutationInterceptor은 CoroutineContext.Element를 확장하는 인터페이스인데, CoroutineContext.Element는 CoroutineContext를 확장하는 인터페이스이다.

 

*CoroutineExceptionHandler 내부 살펴보기

더보기
public interface CoroutineExceptionHandler : CoroutineContext.Element { .. }

public interface CoroutineContext {
    public interface Element : CoroutineContext { .. }
}

CoroutineExceptionHandler은 CoroutineContext.Element 인터페이스를 확장하는 인터페이스이며, CoroutineContext.Element는 CoroutineContext를 확장하는 인터페이스이다.

 

 

그렇다면 CoroutineContext란 무엇인가?

이미 눈치를 챈 사람도 있겠지만, CoroutineContext는 Coroutine이 실행되는 환경이라고 생각하면 된다. 위에 나온 Dispatcher와 CoroutineExceptionHandler 또한 Coroutine이 실행되는 환경의 일부이며, 이 둘 모두는 CoroutineContext에 포함되어 Coroutine이 실행되는 환경으로 설정될 수 있다.

 

CoroutineContext 합치기

Dispatcher와 CoroutineExceptionHandler을 결합해 하나의 Context로 만들어보자. 여기서 우리는 CoroutineContext상의 operator fun plus를 사용한다. 내부 로직은 복잡하니 스킵하도록 하자.

public interface CoroutineContext {
    public operator fun plus(context: CoroutineContext): CoroutineContext 
     ..
}

코드는 다음과 같다.

val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->  }

val coroutineContext = Dispatchers.IO + exceptionHandler

코드를 해석하면 하나의 CoroutineContext에 Dispatcher.IO와 CoroutineExceptionHandler가 들어가 이 Context는 IO Thread에서 실행되는 Exception을 Handling할 수 있게 되었다.

 

이를 시각적으로 표현하면 <그림3>과 같다.

그림3. CoroutineContext 합치기

 

이렇게 만들어진 CoroutineContext는 CoroutineContext가 들어가야 할 자리에 넣음으로써 사용될 수 있다.

그림4. CoroutineContext자리에 합쳐진 Context 넣기

 

CoroutineContext 접근하기

CoroutineContext는 CoroutineContext의 집합이라는 것을 위에서 알아보았다. 이번에는 이러한 집합에서 특정한 CoroutineContext에 접근하는 방식을 알아볼 것이다.

 

위 <그림3>에서는 CoroutineContext를 간단하게 표현하기 위해 요약된 그림을 제공하였다. <그림3>을 조금 더 상세히 표현하면 다음과 같다.

그림4. Coroutine Context

CoroutineContext를 구성하는 두개의 CoroutineContext인 Dispatcher와 CoroutineExceptionHandler가 있고 Dispatcher의 key값은 "keyA"이고 CoroutineExceptionHandler의 key값을 "keyB"라고 해보자. 물론 Key값이 String은 아니지만, 이해하기 편하기 위해 위와 같이 설명한다.

 

위의 그림4에서 ExceptionHandler을 부모 CoroutineContext로부터 가져오고 싶다.

fun main() {
    val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> }

    val coroutineContext = Dispatchers.IO + exceptionHandler // 부모 CoroutineContext = Dispatcher+ExceptionHandler

    val exceptionHandlerFromContext = coroutineContext[exceptionHandler.key] // Key를 통한 자식 CoroutineContext 접근

    if (exceptionHandler === exceptionHandlerFromContext) { // 같은 객체인지 확인하기 위해 동일성 비교
        println(true)
    }
}

그럴경우 아래와 같이 CoroutineContext에 key값을 보내 자식 CoroutineContext의 요청이 가능하다.

<그림5>를 통해 어떤 방식으로 CoroutineContext에 접근이 가능한지 살펴보자.

coroutineContext[exceptionHandler.key]

이로부터 가져온 값을 이전에 결합한 Coroutine Context와 동일성(동일성 비교를 위해 ===를 사용했다) 비교하면 true가 출력된다.

if (exceptionHandler === exceptionHandlerFromContext) { // 같은 객체인지 확인하기 위해 동일성 비교
	println(true)
}
//true 출력

 

이를 그림으로 표현하면 <그림5>와 같다.

그림5. CoroutineContext 접근

 

CoroutineContext에서 CoroutineContext 제거하기

위에서와 같이 자식 CoroutineContext에 접근 가능하면 당연히 제거도 가능하다. CoroutineContext의 제거는 minusKey 메서드를 통해 가능하다.

fun main() {
    val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> }

    val coroutineContext = Dispatchers.IO + exceptionHandler

    val minusContext = coroutineContext.minusKey(exceptionHandler.key)
}

 

이를 그림으로 표현하면 다음과 같다.

 

1. CoroutineExceptionHandler(CoroutineContext) 제거 요청

그림6. key를 이용한 제거 요청

 

2. 제거 완료

그림7. 제거 완료

 

3. 자신 반환 : minusKey를 이용하면 제거된 CoroutineContext가 반환된다.

public fun minusKey(key: Key<*>): CoroutineContext
val minusContext = coroutineContext.minusKey(exceptionHandler.key)

그림8. 자신 반환

반응형

 

이 글의 저작권은 '조세영의 Kotlin World' 에 있습니다. 글, 이미지 무단 재배포 및 변경을 금지합니다.

 

 

Kotlin, Android, Spring 사용자 오픈 카톡

오셔서 궁금한 점을 질문해보세요!
비밀번호 : kotlin22

open.kakao.com