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

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

launch 함수와 async 함수는 매개변수로 context, start, block을 가진다.

여기서 context 매개변수를 살펴보자

앞서 다뤘던 내용 중에서 “3-4. CoroutineDispatcher 사용해 코루틴 실행하기” 파트가 있었다. 이 파트에서는 context 자리에 CoroutineDispatcher 객체가 사용되었다.

이 외에도 context 매개변수에 CoroutineName 객체를 사용할 수도 있다.

이 둘이 context 인자로 사용될 수 있는 이유는 이 두 객체가 CoroutineContext 객체의 구성 요소이기 때문이다.

CoroutineContext는 코루틴을 실행하는 실행 환경을 설정하고 관리하는 인터페이스로 CoroutineDispatcher, CoroutineName, Job 등의 객체를 조합해 코루틴의 실행 환경을 설정한다.

즉, CoroutineContext 객체는 코루틴을 실행하고 관리하는데 핵심적인 역할을 하며, 코루틴의 실행과 관련된 모든 설정은 CoroutineContext 객체를 통해 이뤄진다.

6장에서 다루는 내용

  • CoroutineContext의 구성 요소
  • CoroutineContext 구성 방법
  • CoroutineContext 구성 요소에 접근하기
  • Coroutinecontext 구성 요소를 제거하는 방법

1. CoroutineContext의 구성 요소

CoroutineContext 객체는 CoroutineName, CoroutineDispatcher, Job, CoroutineExceptionHandler의 네 가지 주요한 구성 요소를 가지며, 이들 구성 요소는 다음과 같은 역할을 한다.

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

이들 이외에도 다양한 구성요소가 있지만 주로 이 4가지 구성 요소를 사용해 코루틴을 실행시키기 때문에 이들을 중심으로 살펴본다.


2. CoroutineContext 구성하기

2-1. CoroutineContext가 구성 요소를 관리하는 방법

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

  • 각 구성 요소는 고유한 키를 가짐
  • 키에 중복된 값은 허용되지 않음

CoroutineContext 객체는 각 구성 요소를 한 개씩만 가질 수 있음

2-2. CoroutineContext 구성

CoroutineContext 객체에 구성 요소를 추가하는 방법에 대해 살펴본다.

CoroutineContext 객체는 키-값 쌍으로 구성 요소를 관리하지만 키에 값을 직접 대입하는 방법을 사용하지 않는다.

대신 CoroutineContext 객체 간에 더하기 연산자를 통해 CoroutineContext 객체를 구성한다.

val coroutineContext: CoroutineContext = 
	newSingleThreadContext("MyThread1") + CorotineName("MyCoroutine")
  • newSingleThreadContext(”MyThread1”) → CoroutineDispatcher 객체 생성
  • CoroutineName(”MyCoroutine”) → CoroutineName 객체 생성

이렇게 만들어진 CoroutineContext 객체는 다음과 같은 형태가 된다.

CoroutineName 키CoroutineName(”MyCoroutine”)
CoroutineDispatcher 키newSingleThreadContext(”MyThread”)
Job 키설정되지 않음
CoroutineExceptionHandler 키설정되지 않음
  • 설정되지 않은 값을 설정되지 않은 상태로 유지됨

만들어진 CoroutineContext 객체는 코루틴 빌더 함수의 context 인자로 넘겨 코루틴을 실행하는데 사용할 수 있다.

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

// [MyThread @MyCoroutine#2] 실행
  • 실행 결과를 통해 coroutineContext 객체의 구성 요소를 사용해 코루틴이 실행 되었음을 알 수 있다.

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] 실행
  • launch 코루틴이 사용하는 MyThread로 유지
  • 코루틴의 이름은 NewCoroutine으로 변경됨

코루틴의 이름이 변경된 이유는 CoroutineContext 객체의 각 구성 요소는 고유한 키를 갖고 있으므로 만약 같은 구성 요소에 대해 여러 객체가 입력되면 가장 나중에 들어온 값이 앞의 값을 덮어씌우기 때문이다.

2-4. 여러 구성 요소로 이뤄진 CoroutineContext 합치기

여러 구성 요소로 이뤄진 CoroutineContext 객체 2개가 합쳐지고 2개의 CoroutineContext 객체에 동일한 키를 가진 구성 요소가 있다면 나중에 들어온 값이 선택된다.

이를 확인하기 위해 newSingleThreadContext() 함수로 2개의 CoroutineContext 객체를 만들어 두 객체를 합쳐보자

val coroutineContext1 = CoroutineName("MyCoroutine1") + newSingleThreadContext("MyThread1")
val coroutineContext1 = CoroutineName("MyCoroutine2") + newSingleThreadContext("MyThread2")
val combinedCoroutineContext = coroutineContext1 + coroutineContext2
  • coroutineContext1에 coroutineContext2 객체가 더해지므로 coroutineContext1의 구성 요소가 coroutineContext2의 구성 요소로 덮어씌워짐

위의 예시에서는 두 CoroutineContext 객체가 동일한 구성 요소들을 갖고있었기 때문에 coroutineContext1의 구성 요소들이 coroutineContext2의 구성요소로 덮어씌워졌다.

만약 coroutineContext1이 coroutineContext2가 갖고있지 않은 구성요소를 갖고 있었다면 해당 구성 요소는 덮어씌워지지 않았을 것이다.

2-5. CoroutineContext에 Job 생성해 추가하기

Job 객체는 기본적으로 코루틴 빌더 함수를 통해 자동으로 생성된다.

하지만 Job() 함수를 직접 호출해 생성할 수도 있다.

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

이 코드를 통해 생성된 coroutineContext는 다음과 같은 구조를 가진다.

다만 Job 객체를 직접 생성해 추가하면 코루틴의 구조화가 깨질 수 있다. 따라서 새로운 Job 객체를 생성해 CoroutineContext 객체에 추가하는 것은 주의가 필요하다.

이에 대해서는 7장. 구조화된 동시성에서 자세히 다룰 예정이다.


3. CoroutineContext 구성 요소에 접근하기

CoroutineContext 객체의 구성 요소에 접근하기 위해서는 각 구성 요소가 가진 고유한 키가 필요하다.

각 구성 요소의 키를 얻는 방법부터 알아보자

3-1. CoroutineContext 구성 요소의 키

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

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

public data class CoroutineName.Element {
	public companion object Key: CoroutineContext.Key<CoroutineName>
	...
}

CoroutineName 클래스 내부에 CoroutineContext.Key을 구현하는 Key 객체가 Companion Object로 선언되어 있는 것을 볼 수 있다.

이 Key를 사용하면 CoroutineContext에서 CoroutineName에 접근할 수 있다.

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

Job 인터페이스의 내부에도 Key가 Companion Object로 선언된 것을 볼 수 있다.

이는 CoroutineDispatcher와 CoroutineExceptionHandler도 마찬가지다.

3-2. 키를 사용해 CoroutineContext 구성 요소에 접근하기

1. 싱글톤 키를 사용해 CoroutineContext 구성 요소에 접근하기

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

// CoroutineName(MyCoroutine)

이 코드에는 CoroutineName(”MyCoroutine”)과 Dispatchers.IO로 이뤄진 coroutineContext가 있다.

coroutineContext에 연산자 함수 get(대괄호 대체)의 인자로 CoroutineName.Key를 넘김으로써 coroutineContext를 구성하는 CoroutineName 객체만 가져올 수 있다.

2. 구성 요소 자체를 키로 사용해 구성 요소에 접근하기

CoroutineContext의 주요 구성 요소인 CoroutineName, Job, CoroutineDispatcher, CoroutineExceptionHandler 객체는 Companion Object로 CoroutineContext.Key를 갖고 있기 때문에 Key를 명시적으로 사용하지 않아도 구성 요소 자체를 키로 사용할 수 있다.

따라서 굳이 뒤에 “.Key”를 붙이지 않아도 구성 요소에 접근할 수 있다.

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

// CoroutineName(MyCoroutine)

연산자 함수 get의 인자로 넘긴 인자의 “.Key”를 제거했지만 출력 결과는 동일한 것을 확인할 수 있다.

3. 구성 요소의 key 프로퍼티를 사용해 구성 요소에 접근하기

CoroutineContext 객체의 구성 요소들은 모두 key 프로퍼티를 가진다.

이 key 프로퍼티를 사용해 구성 요소에 접근할 수 있다.

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

// CoroutineName(MyCoroutine)
// Dispatchers.IO

coroutineContext를 구성하는데 사용된 coroutineName과 dispatcher 객체의 key 프로퍼티를 통해 CoroutineContext 객체의 구성 요소에 접근할 수 있음을 알 수 있다.

여기서 중요한 점은 구성 요소의 key 프로퍼티는 Companion Object로 선언된 Key와 동일한 객체를 가리킨다는 것이다.

coroutineName.key와 CoroutineName.Key는 동일한 대상을 가리킨다.

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

// coroutineName.key와 CoroutineName.Key 동일합니다.

동일성 연산자를 통해 coroutineName.key와 CoroutineName.Key가 동일한 객체를 가리킴을 확인할 수 있다.


4. CoroutineContext 구성 요소 제거하기

CoroutineContext 객체는 구성 요소를 제거하기 위한 minusKey 함수를 제공한다.

구성 요소의 Key를 인자로 받아 해당 구성 요소를 제거한 CoroutineContext 객체를 반환한다.

4-1. minusKey 함수를 사용해 구성 요소 제거하기

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

이 coroutineContext에서 CoroutineName 객체를 제거하기 위해서는 coroutineContext에 대해 minusKey 함수를 호출하고 coroutineName을 인자로 넘기면 된다.

이 경우 coroutineContextdptj CoroutineName만 제거돼 반환되며, 반환된 coroutineContext는 deletedCoroutineContext에 할당된다.

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
// JobImple{Active}@65e2dbf3

코드의 출력 결과를 보면 coroutineDispatcher의 CoroutineName이 제거되 null이 출력되고 나머지는 정산적으로 출력되는 것을 확인할 수 있다.

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
// JobImple{Active}@65e2dbf3

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


5. 요약

  1. CoroutineContext 객체는 코루틴의 실행 환경을 설정하고 관리하는 객체로 CoroutineDispatcher, CoroutineName, Job, CoroutineExceptionHandler 등의 객체를 조합해 코루틴 실행 환경을 정의한다.
  2. CoroutineName 객체 → 코루틴의 이름 설정
  3. CoroutineDispatcher 객체→ 코루틴을 스레드로 보내 실행
  4. Job 객체 → 코루틴을 조작하는데 사용
  5. CoroutineExceptionHandler → 코루틴의 예외를 처리
  6. CoroutineContext 객체는 키-값 쌍으로 구성 요소를 관리하며, 동일한 객체에 대해 중복을 허용하지 않는다.
  7. 더하기 연산자(+)를 통해 CoroutineContext의 구성 요소를 조합할 수 있다.
  8. 동일한 키를 가진 구성 요소가 여러개 추가될 경우 나중에 추가된 구성 요소가 이전 값을 덮어씌운다.
  9. 일반적으로 구성 요소의 Companion Object로 선언된 Key 프로퍼티를 사용해 키 값에 접근할 수 있다. 예를 들어 CoroutineName의 키 값은 CoroutineName.Key를 통해 접근할 수 있다.
  10. 키를 연산자 함수 get과 사용해 CoroutineContext 객체를 구성하는 요소에 접근할 수 있다.
  11. CoroutineName, CoroutineDispatcher, CoroutineExceptionHandler는 Companion Object인 Key를 통해 CoroutineContext.Key를 구현하기 때문에 그 자체를 키로 사용할 수 있다.
  12. CoroutineContext 객체의 minusKey 함수를 사용하면 CoroutineContext 객체에서 특정 구성 요소를 제거한 객체를 반환받을 수 있다.

0개의 댓글