🤖안드로이드🤖

[안드로이드] CoroutineContext

bbooyaaa 2025. 11. 5. 00:04

안녕하세요. 오늘은 코틀린 코루틴의 정석 - CH6를 읽고 정리한 글입니다.

 

코루틴 빌더 함수인 launch와 async는 어떻게 선언되어 있을까요?

public fun CoroutineScope.launch(
	context: CoroutineContext = EmptyCoroutineContext,
	start: CoroutineStart = CoroutineStart.DEFAULT,
	block: suspend CoroutineScope.() -> Unit
): Job

public fun <T> CoroutineScope.async(
	context: CoroutineContext = EmptyCoroutineContext,
	start: CoroutineStart = CoroutineStart.DEFAULT,
	block: suspend CoroutineScope.() -> T
): Deferred<T>

launch 함수와 async 함수는 똑같이 매개변수로 context, start, block을 가집니다.

차이로는 launch는 Unit을, async는 제네릭 타입 T를 반환한다는 점이죠.

여기까지는 이전 장에서 모두 다뤘었습니다.

그럼 여기서 궁금한 점은 그래서 CoroutineContext가 무엇이냐죠.

 

context를 봅시다.

2장에서는 context 자리에 CoroutineName 객체를 사용했고,

3장에서는 CoroutineDispatcher 객체를 사용했어요.

다른게 들어갔는데 아무 문제가 없네요? 왜일까요?

이 둘이 context 인자로 사용될 수 있었던 이유는 바로 이들이 CoroutineContext 객체의 구성 요소이기 때문입니다.

 

CoroutineContext는 코루틴을 실행하는 실행 환경을 설정하고 관리하는 인터페이스입니다.

CoroutineDispatcher, CoroutineName, Job 등의 객체를 조합해 실행 환경을 설정해요.

⇒ 코루틴의 실행과 관련된 모든 설정은 CoroutineContext 객체를 통해 이뤄지는거죠.

 

6장에서는 CoroutineContext 객체를 사용해 코루틴의 실행 환경을 설정하고 관리하는 방법을 다룹니다.

 

📌6장에서 다루는 내용
1. CoroutineContext의 구성 요소
2. CoroutineContext 구성 방법
3. CoroutineContext 구성 요소에 접근하기
4. CoroutineContext 구성 요소를 제거하는 방법

 

6.1 CoroutineContext의 구성 요소

CoroutineContext 객체는 CoroutineName, CoroutineDispatcher, Job, CoroutineExceptionHandler의 네 가지 주요한 구성 요소를 가집니다.

 

** 이들 외에도 구성 요소가 더 많지만 주로 이 네 가지로 실행시키기 때문에 이것들 위주로 볼게요.*

 

  1. CoroutineName - 코루틴 이름 설정
  2. CoroutineDispatcher - 코루틴을 스레드에 할당, 실행
  3. Job - 코루틴의 추상체, 코루틴 조작하는데 사용
  4. CoroutineExceptionHandler - 코루틴에서 발생한 예외 처리

1,2,3은 이미 살펴봤고, CoroutineExceptionHandler는 8장에서 다룰 것이기 때문에 이번 장에서는 이 구성요소에 대해 알아보는게 아닌,

그래서 CoroutineContext 객체가 이것들을 어떻게 관리하고 사용하는지를 살펴볼게요.

 

6.2 CoroutineContext 구성하기

6.2.1 CoroutineContext가 구성 요소를 관리하는 방법

CoroutineContext 객체는 키-값 쌍으로 각 구성 요소를 관리합니다.

각 구성 요소는 고유한 키를 가집니다. → 중복된 값은 허용하지 않죠.

따라서 CoroutineContext 객체는 CoroutineName, CoroutineDispatcher, Job, CoroutineExceptionHandler 객체를 한 개씩만 가질 수 있어요.

 

6.2.2 CoroutineContext 구성

그럼 이 객체에 구성 요소를 어떻게 추가할 수 있을까요?

키에 값을 직접 대입하는 방법은 사용하지 않습니다.

대신 CoroutineContext 객체 간에 더하기 연산자(+)를 사용해 객체를 구성해요.

 

val coroutineContext: CoroutineContext = newSingleThreadContext("MyThread") + CoroutineName("MyCoroutine")

이런 식으로요.

그럼 CoroutineContext가 이렇게 만들어지죠.

설정하지 않은 구성요소인 Job, CoroutineExceptionHandler는 설정되지 않은 상태로 유지됩니다.

 

그럼 이 CoroutineContext 객체로 코루틴을 실행해봅시다.

fun main() = runBlocking<Unit>{
	val coroutineContext: CoroutineContext = newSingleThreadContext("MyThread") + CoroutineName("MyCoroutine")
	
	launch(context = coroutineContext){
		println("[${Thread.currentThread().name}] 실행")
	}
}

/*
// 결과:
[MyThread @MyCoroutine#2] 실행
*/

코루틴이 MyThread 스레드를 사용해 실행되고, 코루틴 이름은 MyCoroutine으로 설정된 것을 볼 수 있습니다.

 

** 구성요소가 없는 CoroutineContext는 EmptyCoroutineContext를 통해 만들 수 있어요.*

 

6.2.3 CoroutineContext 구성 요소 덮어씌우기

만약 CoroutineContext 객체에 같은 구성 요소가 둘 이상 더해지면,

나중에 추가된 구성요소가 이전의 값을 덮어 씌웁니다.

 

fun main() = runBlocking<Unit>{
	val coroutineContext: CoroutineContext = newSingleThreadContext("MyThread") + CoroutineName("MyCoroutine")
	val newCoroutineContext: CoroutineContext = coroutineContext + CoroutineName("NewCoroutine")
	
	launch(context = coroutineContext){
		println("[${Thread.currentThread().name}] 실행")
	}
}

/*
// 결과:
[MyThread @NewCoroutine#2] 실행
*/

보시면 스레드는 기존에 설정된 MyThread로 유지되지만

코루틴의 이름은 NewCoroutine으로 변경된 것을 확인할 수 있어요.

 

CoroutineContext 객체의 각 구성 요소는 고유한 키를 가지므로

만약 같은 구성 요소에 대해 여러 객체가 입력되면 나중에 들어온 값이 앞의 값을 덮어씌우는 겁니다.

이렇게요.

⇒ CoroutineContext 객체는 키-값 쌍으로 구성 요소를 관리하기 때문에 같은 구성 요소에 대해서 마지막에 들어온 하나의 값만 취합니다.

 

6.2.4 여러 구성 요소로 이뤄진 CoroutineContext 합치기

그럼 여러 구성 요소로 이뤄진 걸 합치면 어떻게 될까요?

 

CoroutineContext 객체 2개가 합쳐지고,

동일한 키를 가진 구성 요소가 있다면,

나중에 들어온 값이 선택됩니다.

 

val coroutineContext: CoroutineContext = newSingleThreadContext("MyThread1") + CoroutineName("MyCoroutine1")
val coroutineContext: CoroutineContext = newSingleThreadContext("MyThread2") + CoroutineName("MyCoroutine2")

val combinedCoroutineContext = coroutineContext1 + coroutineContext2

coroutineContext1에 coroutineContext2를 더하므로 coroutineContext2의 구성 요소들이 선택됩니다.

이렇게요

덮어씌워지는 거죠.

 

만약 coroutineContext1에 coroutineContext2가 가지지 않은 구성요소가 있었다면 그건 덮어 씌워지지 않습니다.

이렇게요 ㅎ

 

6.2.5 CoroutineContext에 Job 생성해 추가하기

Job 객체는 기본적으로 launch나 runBlocking 같은 코루틴 빌더 함수를 통해 자동 생성됩니다.

하지만 Job()을 호출해 생성할 수도 있어요.

val myJob = Job()
val coroutineContext: CoroutineContext = Dispatchers.IO + myJob

이러면 Job()을 호출해 myJob을 만듭니다.

그럼 이렇게 되죠.

 

** Job 객체를 직접 생성해 추가하면 코루틴의 구조화가 깨집니다. 따라서 생성해서 CoroutineContext 객체에 추가하는 것은 주의가 필요해요. 이에 대해서는 7장에서마저 다루기 때문에, 여기서는 그냥 직접 생성해서 추가할 수 있구나 정도만 알고 넘어가면 됩니다.*

 

6.3 CoroutineContext 구성 요소에 접근하기

CoroutineContext 객체의 각 구성 요소에 접근해봅시다.

그럴려면 각 구성 요소가 가진 고유한 키가 필요해요.

6.3.1 CoroutineContext 구성 요소의 키

CoroutineContext 구성 요소의 키는 CoroutineContext.Key 인터페이스를 구현해 만들 수 있어요.

일반적으로는 CoroutineContext 구성요소의 내부에 키를 싱글톤 객체로 구현합니다.

 

public data class CoroutineName(
	val name: String
): AbstractCoroutineContextElement(CoroutineName){
	public companion object Key: CoroutineContext.Key<CoroutineName>
	...
}

CoroutineName 클래스 구현체입니다.

클래스 내부에 CoroutineContext.Key<CoroutineName>을 구현하는 동반 객체companion object Key가 있는 것을 볼 수 있어요.

이 키를 통해 CoroutineContext에서 CoroutineName에 접근이 가능합니다.

 

public interface Job: CoroutineContext.Element{
	public companion object Key: CoroutineContext.Key<Job>
}

Job 인터페이스 내부에도 Key가 동반 객체로 선언된 것을 볼 수 있어요.

이는 CoroutineDispatcher, CoroutineExceptionHandler도 마찬가지죠.

즉, CoroutineContext 구성 요소의 내부에 선언된 키는 위와 같습니다.

 

6.3.2 키를 사용해 CoroutineContext 구성 요소에 접근하기

그럼 키가 어떻게 선언돼 있는지 봤으니까 각 키를 사용해 직접 접근해봅시다.

 

먼저 싱글톤 키인 CoroutineName.Key를 사용해 접근해볼게요.

fun main() = runBlocking<Unit>{
	val coroutineContext = CoroutineName("MyCoroutine") + Dispatchers.IO
	val nameFromContext = coroutineContext[CoroutineName.Key]
	
	println(nameFromContext)
}

/*
// 결과:
CoroutinName(MyCoroutine)
*/

coroutineContext에 대해 연산자 함수 get의 인자로 CoroutineName.Key를 넘겨 CoroutineName 객체만 가져왔습니다.

 

** get은 연산자 함수이므로 대괄호로 대체 가능해요.*

 

 

이번에는 구성 요소 자체를 키로 사용해서 접근해볼게요.

사실 CoroutineName, Job, CoroutineDisaptcher, CoroutineExceptionHandler 객체는 동반 객체로 CoroutineContext.Key를 구현하는 Key를 가지고 있잖아요.

그래서 Key를 명시적으로 사용하지 않고 그 자체를 키로 사용 가능합니다.

 

fun main() = runBlocking<Unit>{
	val coroutineContext = CoroutineName("MyCoroutine") + Dispatchers.IO
	val nameFromContext = coroutineContext[CoroutineName]
	
	println(nameFromContext)
}

/*
// 결과:
CoroutinName(MyCoroutine)
*/

이렇게요.

.Key를 제거하고 CoroutineName 클래스를 키로 사용했죠.

 

 

이번에는 구성요소의 key 프로퍼티를 사용해서 접근해볼게요.

구성요소들은 모두 key 프로퍼티를 가져요.

fun main() = runBlocking<Unit>{
	val coroutineName = CoroutineName("MyCoroutine")
	val dispatcher = Dispatchers.IO
	val coroutineContext = coroutineName + dispatcher
	
	println(coroutineContext[coroutineName.key])
	println(coroutineContext[dispatcher.key])

}

/*
// 결과:
CoroutinName(MyCoroutine)
Dispatchers.IO
*/

각 구성 요소 인스턴스의 key 프로퍼티를 사용했죠.

 

중요한 점은 구성 요소의 key 프로퍼티는 동반 객체로 선언된 Key와 동일한 객체를 가리킨다는 겁니다.

fun main() = runBlocking<Unit>{
	val coroutineName = CoroutineName("MyCoroutine")
	
	if(coroutineName.key === CoroutineName.Key){
		println("coroutineName.key와 CoroutineName.Key 동일합니다")
	}
}

/*
// 결과:
coroutineName.key와 CoroutineName.Key 동일합니다
*/

이 코드 실행 결과를 보면 둘이 동일한 객체를 가리킨다는 것을 알 수 있어요.

 

6.4 CoroutineContext 구성 요소 제거하기

그럼 이제 제거해봅시다.

제거하기 위해 CoroutineContext 객체는 minusKey 함수를 제공해요.

minusKey 함수는 구성 요소의 키를 인자로 받아 해당 구성 요소를 제거한 CoroutineContext 객체를 반환합니다.

 

6.4.1 minusKey 사용해 구성 요소 제거하기

val coroutineName = CoroutineName("MyCoroutine")
val dispatcher = Dispatchers.IO
val myJob = Job()
val coroutineContext: CoroutineContext = coroutineName + dispatcher + myJob

위와 같이 coroutinContext를 만들었을 때, CoroutineName 객체를 제거하기 위해서는 minusKey 함수를 호출해 CoroutineName을 인자로 넘기면 됩니다.

 

fun main() = runBlocking<Unit>{
	val coroutineName = CoroutineName("MyCoroutine")
	val dispatcher = Dispatchers.IO
	val myJob = Job()
	val coroutineContext: CoroutineContext = coroutineName + dispatcher + myJob
	
	val deletedCoroutineContext = coroutineContext.minusKey(CoroutineName)
	
	println(deletedCoroutineContext[CoroutineName])
	println(deletedCoroutineContext[CoroutineDispatcher])
	println(deletedCoroutineContext[Job])
}

/*
// 결과:
null
Dispatchers.IO
JobImpl{Active}@65e2dbf3
*/

coroutineContext에서 CoroutineName 객체가 제대로 제거된 채 deletedCoroutineContext에 할당된 걸 확인할 수 있습니다.

제거되지 않은 나머지는 잘 설정되어 있습니다.

 

6.4.2 minusKey 함수 사용 시 주의할 점

minusKey를 호출한 CoroutineContext 객체는 그대로 유지되고, 새로운 CoroutineContext 객체가 반환되는 점을 주의해야해요.

fun main() = runBlocking<Unit>{
	val coroutineName = CoroutineName("MyCoroutine")
	val dispatcher = Dispatchers.IO
	val myJob = Job()
	val coroutineContext: CoroutineContext = coroutineName + dispatcher + myJob
	
	val deletedCoroutineContext = coroutineContext.minusKey(CoroutineName)
	
	println(coroutineContext[CoroutineName])
	println(coroutineContext[CoroutineDispatcher])
	println(coroutineContext[Job])
}

/*
// 결과:
CoroutineName(MyCoroutine)
Dispatchers.IO
JobImpl{Active}@65e2dbf3
*/

실행 결과를 보면 minusKey가 호출된 coroutineContext는 구성요소가 제거되지 않기 때문에 CoroutineName 객체가 제거되지 않은 것을 확인할 수 있습니다.

 

6.5 요약

  1. CoroutineContext 객체 = 코루틴의 실행 환경을 설정하고 관리하는 객체
    CoroutineDispatcher, CoroutineName, Job, CoroutineExceptionHandler 등의 객체를 조합해 코루틴 실행 환경 정의
  2. CoroutineContext 구성 요소
    2-1. CoroutineName 객체 - 코루틴 이름 설정
    2-2. CoroutineDispatcher 객체 - 코루틴을 스레드로 보내 실행
    2-3. Job 객체 - 코루틴 조작
    2-4. CoroutineExceptionHandler 객체 - 코루틴 예외 처리
  3. 키-값 쌍으로 구성 요소 관리 → 동일한 키에 대해 중복 값 허용 X
    ⇒ 각 구성 요소를 한 개씩만 가질 수 있음
  4. 더하기 연산자(+)를 사용해 CoroutineContext 구성 요소 조합 가능
  5. 동일한 키를 가진 구성 요소가 여러 개 추가될 경우, 마지막 추가된 구성 요소가 이전 값 덮어씌움. → 마지막 추가 구성 요소만 유효
  6. 일반적으로 구성 요소의 동반 객체로 선언된 key 프로퍼티를 사용해 키 값에 접근 가능 → CoroutineName.Key를 통해 CoroutineName에 접근
  7. 연산자 함수인 get으로도 키 값에 접근 가능 → coroutinContext.get(CoroutineName.Key)
  8. get 연산자 함수는 대괄호로 대체 가능 → coroutinContext[CoroutineName.Key]
  9. 구성 요소는 동반 객체인 Key를 통해 CoroutineContext.Key를 구현하기 때문에 그 자체로 키로 사용이 가능 → coroutinContext[CoroutineName.Key] === coroutinContext[CoroutineName]
  10. minuKey 함수를 사용하면 CoroutineContext 객체에서 특정 구성 요소를 제거한 객체를 반환받음

이렇게 오늘은 CoroutineContext에 대해서 알아보았어요.

이전 장에서 너무나도 궁금했던 그래서 CoroutineContext가 뭔데? 에 대한 궁금증을 풀 수 있어서 좋았습니다.

 

다음에는 CH7 - 구조화된 동시성으로 찾아오겠습니다.

다음 챕터도 화이팅 ~!