[Kotlin/코루틴] CoroutineContext 중 Job과 Dispatcher 정리하기
yujinius2025. 1. 15. 00:57
Coroutine은 세 가지 주요 구성 요소로 이루어져 있다
바로 CoroutineScope, CoroutineContext, 그리고 CoroutineBuilder이다.
코루틴 구성 요소 3가지
CoroutineScope: 코루틴의 실행 범위를 정의.
CoroutineContext: 디스패처(Dispatcher)와 잡(Job) 등 코루틴의 실행 환경을 포함.
CoroutineBuilder: 코루틴을 생성하고 실행하는 빌더(launch, async 등).
이번 포스팅에서는 코루틴의 동작을 정의하는 CoroutineContext에 대해 학습한 것을 정리해보겠다.
🌠 CoroutineContext
CoroutineContext는 다음 요소들을 사용하여 코루틴의 동작을 정의한다.
Job : 코루틴 수명 주기 제어
CoroutineDispatcher : 적절한 스레드에 작업 전달
CoroutineName : 디버깅에 유용한 코루틴의 이름
CoroutineExceptionHandler : 포착되지 않은 예외를 처리
오늘은 여기에서 Job과 CoroutineDispatcher에 대해 정리해보겠다.
✨ Job 코루틴의 생명주기 제어 (feat. Deffered, SupervisorJob)
1. Job이란?
Job은 코루틴의 생명주기를 제어하는 중요한 구성 요소이다.
모든 코루틴에는 하나의 Job 인스턴스가 있으며, 이는 해당 코루틴의 상태(예: 활성, 취소, 완료 등)를 나타내고 제어하는 역할을 한다.
Job의 주요 기능:
코루틴 상태 추적:
Job은 코루틴의 상태를 나타낸다. (예: Active, Completing, Cancelled, Completed)
취소(Cancellation):
Job.cancel()을 호출하여 해당 코루틴을 취소할 수 있다.
취소는 부모-자식 관계를 통해 전파된다.
구조적 동시성(Structured Concurrency):
Job은 부모와 자식 관계를 가지며, 부모 코루틴이 종료되면 자식 코루틴도 모두 취소된다.
완료 대기:
Job.join()을 호출하여, 해당 코루틴이 완료될 때까지 대기할 수 있다.
2. Deferred와 Job의 차이
Deferred는 Job을 확장한 인터페이스로, 코루틴이 결과값을 반환할 수 있도록 한다.
즉, Deferred는 Job의 모든 기능을 상속받으며, 추가적으로 결과값을 처리할 수 있는 기능을 제공한다.
특징 Job Deferred
기능
코루틴의 생명주기 제어
생명주기 제어 + 결과값 반환
결과값 반환 여부
없음
있음 (await() 메서드 사용)
예외 전파
부모-자식 관계를 통해 전파
예외가 발생하면 즉시 호출부로 전파
주요 사용 예
Fire-and-Forget 작업
결과값이 필요한 작업
Job 예제:
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
delay(1000)
println("Job completed")
}
println("Waiting for job to finish...")
job.join() // 코루틴이 완료될 때까지 대기
println("Job finished")
}
Deferred 예제:
import kotlinx.coroutines.*
fun main() = runBlocking {
val deferred = async {
delay(1000)
"Deferred result"
}
println("Waiting for result...")
val result = deferred.await() // 결과값 반환
println("Result: $result")
}
3. SupervisorJob이란?
SupervisorJob은 부모-자식 관계에서 예외 전파를 제어하기 위해 사용되는 특수한 Job이다.
기본 Job에서는 자식 코루틴 중 하나에서 예외가 발생하면 부모와 다른 자식들도 모두 취소된다. 하지만 SupervisorJob에서는 자식의 예외가 부모나 다른 자식들에게 전파되지 않는다.
SupervisorJob의 특징:
독립적인 자식 작업:
자식 코루틴 중 하나에서 예외가 발생해도, 다른 자식 코루틴은 영향을 받지 않고 계속 실행된다.
부모-자식 관계의 수정:
기본 Job은 부모-자식 관계에서 예외를 전파하지만, SupervisorJob은 전파를 막는다.
CPU 스케줄러 내부에 포함된 것으로, 단기 스케줄러가 선택한 프로세스에 실질적으로 프로세서를 할당하는 역할을 한다.
프로세스의 레지스터를 적재하고(문맥교환), 운영체제 모드(Kernel Mode)에서 사용자 상태(User Mode)로 전환시켜주며 프로세스가 다시 시작할 때 사용자 프로그램이 올바른 위치를 찾을 수 있도록 한다. 👉🏻 dispatcher의 처리속도를 빠르면 빠를수록 좋다.
코루틴에서의 Dispatcher : CoroutineDispatcher
코루틴에서의 Dispatcher인 CoroutineDispatcher는 코루틴이 어떤 스레드 또는 스레드 풀(Thread Pool)에서 실행될지를 결정하는 구성 요소이다.
Dispatchers는 다양한 CoroutineDispatcher 구현체를 그룹화한 객체로, 코루틴이 실행될 스레드나 스레드 풀을 결정한다.
Kotlin에서는 코루틴 실행을 위한 표준 디스패처를 제공한다.
1) Dispatchers.Default
기본 디스패처로, launch, async 등 표준 빌더에서 디스패처를 명시하지 않을 경우 사용
주로 CPU 집약적인 작업에 최적화되어 있으며, 가용 CPU 코어 수를 기반으로 스레드 풀을 구성
예: 계산 작업, 데이터 처리, 알고리즘 실행.
2) Dispatchers.IO
블로킹 I/O 작업(파일 읽기/쓰기, 네트워크 요청 등)을 처리하도록 설계된 디스패처
공유 스레드 풀을 사용하며, 필요에 따라 추가 스레드를 생성하여 대규모 I/O 작업도 처리할 수 있다.
안드로이드 환경에서는 네트워크 작업이나 데이터베이스 접근 시 자주 사용된다.
3) Dispatchers.Main
UI 객체와 작업할 때 사용하는 디스패처로, 주로 메인 스레드에 바인딩된다.
일반적으로 싱글 스레드로 동작하며, UI 업데이트 작업에 사용된다.
Android 환경에서 가장 많이 사용되는 디스패처 중 하나이다.
4) Dispatchers.Unconfined
특정 스레드에 고정되지 않는 디스패처이다.
코루틴의 초기 실행은 현재 호출 프레임에서 수행되며, 이후에는 suspend 함수에 따라 실행될 스레드가 결정된다.
특정 스레드 정책을 요구하지 않는 간단한 작업에 적합하지만, 예측하기 어려운 실행 흐름 때문에 일반적으로 권장되지 않는다.
중첩된 코루틴은 이벤트 루프(event-loop)를 형성하여 스택 오버플로우를 방지한다.
주요 함수: shutdown
Default와 IO 디스패처를 종료하고, 관련된 모든 스레드를 중지시킨다.
종료 후, 새로운 작업 요청을 모두 거부하며, 시간 관련 작업(delay, withTimeout)이나 기타 디스패처에서 거부된 작업도 처리하지 않는다.
주의: 이 함수는 민감한 API(@DelicateCoroutinesApi)로 분류되며, 일반적으로 사용이 권장되지 않는다.
위에서 언급되는 ThreadPool이란 무엇인가?
쓰레드 풀은 미리 일정 개수의 쓰레드를 생성하여 관리하는 기법이다.
쓰레드 풀을 사용하면 스레드 생성 및 삭제에 따른 오버헤드를 줄일 수 있으며, 특정 시점에 동시에 처리할 수 있는 작업의 개수를 제한할 수 있다. 이를 통해 시스템의 자원을 효율적으로 관리하고 성능을 향상시킬 수 있다.
스레드 풀은 작업처리에 사용되는 스레드를 제한된 개수만큼 정해놓고 작업큐 (Queue)에 들어오는 작업들을 하나씩 스레드가 맡아 처리한다. 그렇게 하면 작업처리 요청이 폭증되어도 스레드의 전체개수가 늘어나지 않으므로(제한해서 하나씩 처리하기 때문) 시스템 성능이 급격히 저하되지 않는다.
두 코드의 실행 결과를 보면 사용하는 스레드의 이름이 DefaultDispatcher-worker-1,3,4로 동일하다. 하지만 실제 공유 스레드풀을 보면 두 디스패처가 사용하는 스레드풀은 완전히 달라서 겹칠 수 없다. 그런데 도대체 왜 이름이 같은것일까?
Dispatchers.IO와 Dispatchers.Default가 사용하는 스레드의 이름이 유사한 것은 사실이다. 둘 다 "DefaultDispatcher-worker-X" 형식의 이름을 사용한다. …
이것은 이 디스패쳐들이 스레드를 만들 때 사용하는 공유 스레드풀 때문이다. 코루틴 라이브러리는 스레드의 생성과 관리를 효율적으 로 할 수 있도록 애플리케이션 레벨의 공유 스레드풀을 제공한다. 이 공유 스레드풀에서는 스레드를 무제한으로 생성할 수 있고, 이 공유 스레드풀을 사용해 각각의 디스패처에서 사용할 스레드를 생성한다.
그림을 보면 DispatchersDefault의 스레드와 Dispatchers IO의 스레드가 나누어져 있는 것을 볼 수 있다. 이것이 바로 스레드의 이름의 앞부분이 동일했던 이유다.