Kotlin

[Kotlin] CoroutineContext

꾸준함. 2024. 10. 3. 12:48

CoroutineContext

  • 코틀린에서 코루틴을 실행할 때 사용하는 컨텍스트를 나타내는 인터페이스
  • Spring MVC와 같이 thread-per-request 모델에서는 ThreadLocal을 사용하지만 Reactor나 코루틴처럼 각 요청을 처리할 때마다 수행하는 쓰레드가 변경되기 때문에 특정 쓰레드에 종속되는 ThreadLocal에 접근 불가
    • 이에 따라 Reactor에서는 Context, 코루틴에서는 CoroutineContext를 사용하여 파이프라인 context를 저장
    • CoroutineContext는 코루틴의 동작을 커스터마이징하고, 실행 쓰레드나 예외 처리, 디버깅 정보 등을 포함하는 요소들을 담고 있음

 

1. Continuation

  • suspend 함수는 다양한 쓰레드에서 실행되기 때문에 ThreadLocal 사용 불가
  • 앞선 게시글에서 설명한 Continuation을 통해 실행 상태와 관련된 데이터를 전달
    • Continuation은 CoroutineContext를 포함하고 해당 코루틴의 실행 환경과 필요한 정보를 지속적으로 전달
    • 코루틴이 suspend 함수 내에서 중단되고 다시 재개될 때, 해당 코루틴의 전체 상태를 이어받아야 하므로, CoroutineContext는 해당 정보를 계속 유지하고 있어야 함

 

Continuation

 

2. CoroutineContext 접근 방법

  • runBlocking, launch, aysnc와 같은 CoroutineScope 내부에서는 CoroutineScope.coroutineContext를 통해 접근 가능

 

 

  • Continuation에 접근 가능할 경우 Continuation.coroutineContext를 통해 접근 가능

 

 

  • suspend 함수 내부에서는 coroutineContext를 통해 접근 가능

 

 

2.1 예시 코드

 

 

부연 설명

  • runBlocking CoroutineScope 내부에서 coroutineContext 접근
  • suspend 함수 내부에서 coroutineContext 접근
  • suspendCoroutine 내부에서 continuation을 인자로 받고 continuation.context로 접근

 

3. CoroutineContext 객체

  • Key는 Element를 구분할 때 사용하는 식별자
  • CoroutineContext는 여러 Element를 포함하며 Element의 개수에 따라 다른 객체로 존재
    • EmptyCoroutineContext: Element가 하나도 없는 상태
    • Element: Element가 하나인 상태로 Element 그 자체
    • CombinedContext: Element가 두 개 이상일 때

 

EmptyCoroutineContext
Element
CombinedContext

 

4. CoroutineContext 전파

 

4.1 suspend 함수 간 전파

  • suspend 함수에서 다른 suspend 함수를 호출하는 경우 외부 suspend 함수의 Continuation를 전달하며 이를 통해 외부 Continuation의 CoroutineContext가 내부 suspend 함수에 전달

 

 

4.2 withContext

  • 현재 context에 특정 Element만 추가해서 실행하고 싶을 경우 사용
  • 현재 코루틴의 coroutineContext에 인자로 전달된 context를 merge
  • 새로운 Job을 생성해 주입

 

 

 

부연 설명

  • runBlocking 내부에서 withContext 실행
  • withContext는 runBlocking의 coroutineContext를 merge 하여 CoroutineName을 오버라이드하고 UndispatchedCoroutineJob을 새로 생성

 

5. CoroutineContext 종류

 

5.1 CoroutineName

  • 디버깅에 이용되는 Element

 

 

 

5.2 Job

  • 코루틴의 생명주기 관리
  • Job은 active, completed, cancelled와 같은 여러 상태를 갖음
  • start, cancel을 통해 명시적으로 시작과 취소 가능
  • parent, children을 통해 다른 코루틴의 생명주기도 관리
  • launch, async 등의 코루틴 비리더를 통해 자식 Job 생성 가능

 

https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/

 

Job

Job A background job. Conceptually, a job is a cancellable thing with a life-cycle that culminates in its completion. Jobs can be arranged into parent-child hierarchies where cancellation of a parent leads to immediate cancellation of all its children recu

kotlinlang.org

 

5.3 ReactorContext

  • ReactorContext를 통해 Reactor의 ContextView를 다른 suspend 함수에 전달

 

 

부연 설명

  • contextWrite을 통해 context 주입
  • launch 블록 내 coroutineContext의 ReactorContext로 접근하여 해당 context를 기반으로 새로운 reactorContext 생성
  • ReactorContext(newContext)로 launch에 전달
  • Mono에서 contextWrite을 통해 coroutineContext로 전달된 reactorContext를 주입하고 출력

 

6. 코루틴 예외 처리

 

6.1 aysnc의 예외 처리

  • CoroutineScope aysnc 내에서 예외가 발생할 경우 동기 코드처럼 try-catch를 통해 예외 처리 가능

 

 

 

6.2 launch의 예외 처리

  • CoroutineScope launch 내에서 예외가 발생하는 경우 async와 달리 try-catch 블록에 잡히지 않고 쓰레드의 UncaughtExceptionHandler를 통해 출력
    • launch 전체를 try-catch로 감싸더라도 UncaughtExceptionHandler를 통해 출력
    • 함수처럼 exception이 전파되는 구조가 아니라 자식 Job에서 부모 Job으로 cancellation이 전파되며 함께 exception이 전달되기 때문에 일반적인 try-catch로 예외처리 불가

 

 

6.3 CoroutineExceptionHandler

  • CoroutineExceptionHandler를 통해 root coroutine의 예외 처리 가능
    • launch에 적용 가능
    • async는 예외를 Deferred를 통해 전달하기 때문에 적용 불가
    • runBlocking에도 적용 불가

 

  • handler는 반드시 root 코루틴 launch에 제공해야 함
    • 중간 launch에 제공할 경우 예외 처리 불가

 

 

 

7. CoroutineDispatcher

  • 코루틴이 어느 쓰레드에서 실행될지 결정하는 Element

 

 

  • CoroutineDispatcher는 Default, Main, Unconfined, IO 등을 미리 만들어 제공
    • 하지만 Main Dispatcher는 라이브러리를 통해서 주입받아야 하는데 kotlinx-coroutines-core에서 지원하지 않기 때문에 Main은 사용 불가
    • Default: 기본으로 사용되는 Dispatcher로 CPU 코어 수만큼 고정된 크기를 갖는 쓰레드 풀을 제공하며 CPU Bound Blocking에 적합
    • IO: 기본적으로 최대 64개까지 늘어나는 가변 크기를 갖는 쓰레드 풀을 제공하며 I/O Bound Blocking에 적합 (Reactor의 boundedElastic 쓰레드 풀과 비슷)
    • Unconfined: 처음엔 caller 쓰레드 기준으로 실행되고 이후엔 마지막으로 실행된 suspend 함수의 쓰레드를 따라가기 때문에 어떤 쓰레드에서 코루틴이 실행될지 예상하기 힘듦 (일반적으로 사용 X)

 

 

 

 

부연 설명

  • runBlocking은 BlockingEventLoop.dispatcher를 사용
  • CoroutineScope를 만들고 Dispatcher를 전달하지 않아 Default로 실행
  • Default와 IO Dispatcher는 DefaultDispatcher-worker-1에서 동일하게 동작
    • IO와 Default는 쓰레드 풀을 공유하지만 동시에 수행 가능한 쓰레드 수는 Default는 고정인 반면 IO는 가변
    • CPU Bound Blocking 작업은 CPU 숫자보다 늘려도 의미가 없는 반면 IO Bound Blocking 작업은 쓰레드를 늘리면 그만큼 더 많은 IO 수행 가능

 

8. ExecutorCoroutineDispatcher

  • 특정한 쓰레드 혹은 고정된 개수의 쓰레드 풀을 갖는 Dispatcher 생성
  • N개의 쓰레드를 갖는 Executor를 생성하고 asCoroutineDispatcher를 통해 CoroutineDispatcher로 변환
  • ExecutorCoroutineDispatcher는 내부에 Executor를 포함시키기 때문에 명시적으로 close를 호출하여 Executor를 종료해야 함

 

 

부연 설명

  • runBlocking은 main 쓰레드에서 실행
  • withContext는 Dispatchers.IO로 실행
  • delay는 DefaultExecutor로 실행

 

참고

패스트 캠퍼스 - Spring Webflux 완전 정복 : 코루틴부터 리액티브 MSA 프로젝트까지

반응형

'Kotlin' 카테고리의 다른 글

[Kotlin] CoroutineScope  (0) 2024.10.03
[Kotlin] 코루틴 (Coroutine) 기초  (0) 2024.10.02
[Kotlin] 코루틴 (Coroutine) 개요  (0) 2024.10.02
[Kotlin] lateinit과 by lazy의 차이점  (2) 2022.07.13