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 {
// 커스텀 스코프에서 코루틴 실행
}
CoroutineScope를 직접 생성하여 애플리케 이션 전체에서 사용할 수 있는 커스텀 스코프를 만들 수 있습니다.- 디스패처, 예외 처리, 이름 등을 설정하여 특정 용도에 맞게 스코프를 최적화할 수 있습니다.
5.2 SupervisorJob을 사용한 예외 격리
launch(SupervisorJob()) {
// 첫 번째 자식 코루틴
launch {
delay(100)
throw RuntimeException("에러 발생!")
}
// 두 번째 자식 코루틴 - 위의 예외에 영향 받지 않음
launch {
delay(200)
println("이 코드는 실행됩니다.")
}
}
SupervisorJob은 일반Job과 달리 자식 코루틴이 실패해도 다른 자식 코루틴은 취소되지 않습니다.- 서로 독립적인 여러 작업을 실행할 때 유용합니다.
5.3 컨텍스트 요소 커스터마이징
// 커스텀 코루틴 컨텍스트 요소 정의
class LoggingContext(val id: String) : AbstractCoroutineContextElement(Key) {
companion object Key : CoroutineContext.Key<LoggingContext>
}
// 커스텀 요소 사용
launch(LoggingContext("REQ-123")) {
val requestId = coroutineContext[LoggingContext]?.id
println("요청 ID: $requestId") // 출력: 요청 ID: REQ-123
}
- 자신만의 컨텍스트 요소를 정의하여 코루틴에 추가 정보를 전달할 수 있습니다.
- 로깅, 트레이싱, 요청 식별 등 다양한 용도로 활용할 수 있습니다.