CoroutineContext
앞서 우리는 다음의 내용들을 배웠다.
- Dispatcher: 코루틴이 실행될 스레드 풀을 잡고 있는 관리자
- CoroutineExceptionHandler: 코루틴에서 Exception이 생겼을 때의 처리기
그런데 이 두 가지 요소는 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>과 같다.
이렇게 만들어진 CoroutineContext는 CoroutineContext가 들어가야 할 자리에 넣음으로써 사용될 수 있다.
CoroutineContext 접근하기
CoroutineContext는 CoroutineContext의 집합이라는 것을 위에서 알아보았다. 이번에는 이러한 집합에서 특정한 CoroutineContext에 접근하는 방식을 알아볼 것이다.
위 <그림3>에서는 CoroutineContext를 간단하게 표현하기 위해 요약된 그림을 제공하였다. <그림3>을 조금 더 상세히 표현하면 다음과 같다.
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>와 같다.
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) 제거 요청
2. 제거 완료
3. 자신 반환 : minusKey를 이용하면 제거된 CoroutineContext가 반환된다.
public fun minusKey(key: Key<*>): CoroutineContext
val minusContext = coroutineContext.minusKey(exceptionHandler.key)