1. 코루틴 컨텍스트란?
- 코루틴 컨텍스트(CoroutineContext)는 코틀린 코루틴의 실행 환경을 정의하는 인터페이스입니다.
- 코루틴이 어떤 스레드에서 실행될지, 어떤 예외 처리기를 사용할지, 코루틴의 생명주기를 어떻게 관리할지 등의 정보를 포함합니다.
- 코루틴 컨텍스트는 여러 요소(Element)의 집합으로, 각 요소는 특정 측면(스레드 정책, 예외 처리 등)을 담당합니다.
CoroutineContext
는 일종의 맵(Map)과 유사한 인덱싱된 집합으로, 키(Key)와 값(Element)의 쌍으로 구성됩니다.
정보
코루틴 컨텍스트는 코루틴이 실행되는 환경과 정책을 결정합니다. 자바의 ExecutorService
나 스레드 관련 설정보다 더 풍부한 정보를 담고 있어 코루틴의 강력한 기능을 가능하게 합니다.
2. 코루틴 컨텍스트의 주요 구성 요소
2.1 CoroutineDispatcher (디스패처)
- 코루틴이 어떤 스레드 또는 스레드 풀에서 실행될지 결정합니다.
- 코루틴의 실행 컨텍스트를 제공하는 가장 기본적인 요소입니다.
- Dispatchers 참고
2.2 Job
- 코루틴의 생명주기를 관리하는 요소입니다.
- 코루틴의 상태를 추적하고, 코루틴을 취소하거나 완료를 대기할 수 있습니다.
- 부모-자식 관계를 통해 구조화된 동시성(structured concurrency)을 지원합니다.
2.2.1 Job 생성 및 사용
// 코루틴 빌더로 Job 생성
val job = launch {
// 작업 수행
}
// 명시적 Job 생성
val job = Job()
launch(job) {
// job에 연결된 코루틴
}
// Job 상태 확인
println("Job is active: ${job.isActive}")
println("Job is completed: ${job.isCompleted}")
println("Job is cancelled: ${job.isCancelled}")
// Job 완료 대기
job.join() // 코루틴 내부에서 호출
// Job 취소
job.cancel("취소 이유")
2.2.2 Job의 상태
- Job은 다음과 같은 상태를 가집니다:
New
: 아직 실행되지 않은 상태Active
: 실행 중인 상태Completing
: 작업이 완료되고 있는 상태Completed
: 정상적으로 완료된 상태Cancelling
: 취소 중인 상태Cancelled
: 취소된 상태
2.2.3 부모-자식 관계
val parentJob = launch {
// 부모 코루틴
val childJob = launch {
// 자식 코루틴
}
// 부모가 취소되면 자식도 자동으로 취소됩니다
}
// 부모를 취소하면 모든 자식도 취소됩니다
parentJob.cancel()
- 코루틴은 부모-자식 관계를 통해 구조화된 동시성을 지원합니다.
- 부모 코루틴이 취소되면 모든 자식 코루틴도 자동으로 취소됩니다.
- 자식 코루틴이 예외로 실패하면 부모 코루틴도 취소됩니다.
2.3 CoroutineName
- 디버깅을 위해 코루틴에 이름을 부여하는 요소입니다.
- 로그에서 특정 코루틴을 식별하는 데 유용합니다.
launch(CoroutineName("데이터로딩코루틴")) {
println("코루틴 이름: ${coroutineContext[CoroutineName]?.name}")
// 출력: 코루틴 이름: 데이터로딩코루틴
}
2.4 CoroutineExceptionHandler
- 코루틴 내에서 발생한 예외를 처리하는 요소입니다.
- 특히 루트 코루틴의 예외를 처리할 때 중요합니다.
val exceptionHandler = CoroutineExceptionHandler { context, exception ->
println("코루틴 예외 발생: $exception")
println("코루틴 컨텍스트: $context")
}
// 예외 핸들러 사용
launch(exceptionHandler) {
throw RuntimeException("에러 발생!")
}
경고
CoroutineExceptionHandler
는 루트 코루틴(다른 코루틴의 자식이 아닌 코루틴)에서만 동작합니다. 자식 코루틴의 예외는 부모로 전파되므로, 부모 코루틴에 예외 핸들러를 설정해야 합니다.
3. 코루틴 컨텍스트 결합과 상속
3.1 컨텍스트 요소 결합
// 여러 컨텍스트 요소 결합
val combinedContext = Dispatchers.IO + CoroutineName("네트워크 작업") + exceptionHandler
launch(combinedContext) {
// 결합된 컨텍스트에서 실행
}
- 코루틴 컨텍스트는
+
연산자를 사용하여 결합할 수 있습니다. - 결합 시 동일한 키를 가진 요소가 있으면 오른쪽 요소가 왼쪽 요소를 대체합니다.
3.2 컨텍스트 상속과 수정
// 부모로부터 컨텍스트 상속
launch {
// 부모 컨텍스트 사용
launch(Dispatchers.IO) {
// 부모에서 상속받은 컨텍스트에 Dispatchers.IO만 변경
}
withContext(Dispatchers.Default) {
// 임시로 디스패처만 변경
}
}
- 자식 코루틴은 부모 코루틴의 컨텍스트를 상속받습니다.
- 자식 코루틴에서 특정 요소를 변경할 수 있지만, 부모 코루틴의
Job
은 항상 새로운 자식Job
으로 대체됩니다. withContext
를 사용하여 현재 코루틴 내에서 일시적으로 컨텍스트를 변경할 수 있습니다.
4. 컨텍스트 접근과 활용
- CoroutineContext는 컬렉션과 비슷하기 때문에 get을 이용해 유일한 키를 가진 원소를 찾을 수 있습니다.
- 추가적으로 대괄호를 사용해 CoroutineContext의 특정 요소에 접근할 수 있습니다.
- CoroutineContext에서는 모든 원소를 식별할 수 있는 유일한 키를 가지고 있습니다.
4.1 현재 컨텍스트 접근
launch {
// 전체 컨텍스트 접근
println("현재 컨텍스트: $coroutineContext")
// 특정 요소 접근
val dispatcher = coroutineContext[ContinuationInterceptor]
val job = coroutineContext[Job]
val name = coroutineContext[CoroutineName]?.name ?: "unnamed"
println("디스패처: $dispatcher")
println("Job: $job")
println("이름: $name")
}
- 코루틴 내에서
coroutineContext
속성을 통해 현재 컨텍스트에 접근할 수 있습니다. - 인덱싱 연산자
[key]
를 사용하여 특정 요소에 접근할 수 있습 니다. coroutineContext[CoroutineName]
- CoroutineName을 찾기 위해서는 CoroutineName을 사용하면 됩니다.
- 여기서 CoroutineName은 타입이 아닌 캠패니언 객체입니다.
- 클래스의 이름이 컴패니언 객체에 대한 참조로 사용되는 코틀린 언어 특징 때문에
[CoroutineName]
은[CoroutineName.key]
가 됩니다.
4.2 withContext를 사용한 컨텍스트 전환
launch(Dispatchers.Main) {
// UI 스레드에서 실행
updateLoadingState(true)
val result = withContext(Dispatchers.IO) {
// IO 스레드에서 실행
api.fetchData() // 네트워크 요청
}
// 다시 UI 스레드로 돌아옴
updateLoadingState(false)
displayResult(result)
}
withContext
를 사용하면 코루틴 내에서 컨텍스트를 일시적으로 변경할 수 있습니다.- 주로 스레드를 전환 하는 데 사용되며, UI 업데이트와 백그라운드 작업을 함께 수행할 때 유용합니다.
- 코드의 가독성을 높이고 콜백 패턴을 피할 수 있습니다.
4.3 isActive 속성 활용
launch {
while (isActive) { // isActive는 coroutineContext[Job]?.isActive의 축약형
// 취소 가능한 반복 작업
delay(1000)
println("작업 수행 중...")
}
}
isActive
는 현재 코루틴이 활성 상태인지(취소되지 않았는지) 확인하는 속성입니다.- 장시간 실행되는 작업이나 루프에서 코루틴의 취소 여부를 확인하는 데 유용합니다.
4.4 컨텍스트 원소 제거
minusKey
함수를 사용하여 특정 키를 가진 요소를 제거할 수 있습니다.- 원본 컨텍스트는 불변(immutable)이므로 변경되지 않고, 대신 새로운 컨텍스트가 반환됩니다.
- 주로 필요하지 않은 요소를 제거하거나 특정 동작을 재정의하기 전에 기존 요소를 제거할 때 유용합니다.
예시
launch {
// 현재 컨텍스트 확인
println("기존 컨텍스트: $coroutineContext")
// 특정 요소 제거
val newContext = coroutineContext.minusKey(CoroutineName.Key)
// 제거된 컨텍스트 확인
println("요소 제거 후 컨텍스트: $newContext")
// 새 컨텍스트로 코루틴 시작
withContext(newContext) {
println("현재 코루틴 이름: ${coroutineContext[CoroutineName]?.name ?: "이름 없음"}")
}
}
5. 고급 컨텍스트 활용 패턴
5.1 코루틴 스코프 커스터마이징
// 커스텀 코루틴 스코프 생성
val customScope = CoroutineScope(
Dispatchers.Default +
SupervisorJob() +
CoroutineName("CustomScope") +
exceptionHandler
)
// 커스텀 스코프 사용
customScope.launch {
// 커스텀 스코프에서 코루틴 실행
}