launch와 async 선언부의 첫 매개변수는 CoroutineContext다.
우리는 앞서 CoroutineContext의 자리에 CoroutineDispatcher, CoroutineName을 넣었다.
1) CoroutineDispatcher을 넣었을 때 코루틴이 해당 CoroutineDispatcher에 실행 요청됐다.
2) CoroutineName을 넣었을 때 코루틴에 이름이 설정됐다.

그럼 CoroutineContext가 무엇이길래 코루틴을 설정하는데 사용되는걸까?
그리고 CoroutineDispatcher와 CoroutineName을 한 번에 설정할 수 있는 방법에 대해서도 알아보자.
코루틴 컨텍스트(Coroutine Context)는 코루틴이 실행되는 환경을 정의하는 요소들로 이루어진 정보 집합(인터페이스)이다.
코루틴이 어떤 스레드나 디스패처에서 실행되는지, 실행 중의 상태 정보나 특정 작업 관련 데이터를 담고 있다. 각 코루틴은 고유한 컨텍스트를 가지고 있으며, 이 컨텍스트는 코루틴의 동작 방식과 실행 환경을 제어한다.

실제로는 더 많이 있지만, 거의 4개만 사용되므로 얘네만 알아도 충분하다.

CoroutineContext 객체는 구성요소에 대한 키-값 쌍으로 구성요소를 관리한다.
각 구성요소는 고유한 키를 가지며, 키에 대한 중복된 값은 허용하지 않는다.
CoroutineContext 객체에 구성 요소를 추가하기 위해서는 더하기 연산자(+)를 사용하면 된다.
fun main() = runBlocking<Unit> {
val coroutineContext = CoroutineName("YoungNudy Coroutine") + newSingleThreadContext("YoungNudy Thread")
launch(context = coroutineContext) {
println("[${Thread.currentThread().name}] 실행")
}
}

코드에서 의도한대로 코루틴의 이름과 스레드의 이름이 적용되고, 출력할 수 있다.
newSingleThreadContext는 Kotlin의 코루틴에서 단일 스레드 컨텍스트를 생성하는 함수다.
이 함수를 사용하면 지정한 스레드에서만 코루틴이 실행되도록 할 수 있다.
newSingleThreadContext는 CoroutineDispatcher의 일부이다.
Dispatchers.IO와 Dispatchers.Default는 Kotlin에서 미리 정의된 디스패처다. 이들은 특정 작업 유형에 최적화되어 있으며, 내부적으로 적절한 스레드 풀을 관리하여 리소스를 효율적으로 사용할 수 있도록 설계되어 있다.
반면에 newSingleThreadContext는 사용자가 필요에 따라 새롭게 단일 스레드를 생성하는 방법이다. 이 경우, 해당 스레드는 사용자가 직접 생성하고 관리해야 하며, 특정 상황에서만 사용되는 것이 일반적이다.
newSingleThreadContext는 다음과 같은 경우에 유용하다.
하지만 일반적인 경우에는 기본 디스패처들을 사용하는 것이 리소스 관리 측면에서 더 효율적이므로 꼭 필요한 때만 쓰는 것이 좋다.
CoroutineContext 객체에 같은 구성 요소가 둘 이상 더해지면, 나중에 추가된 구성요소가 이전 값을 덮어씌운다.
ex) YoungNudy Coroutine -> 21Savage Coroutine = 21Savage Coroutine
여러 구성요소로 이뤄진 CoroutineContext 두개가 합쳐질 때도 같은 키를 가진 구성요소가 있다면, 나중에 들어온 구성 요소가 먼저 들어온 구성 요소를 덮어 씌운다



CoroutineContext 구성요소에 접근하기 위해서는 구성요소가 가진 고유한 키가 필요하다.
CoroutineContext 구성요소의 키는 CoroutineContext.Key 인터페이스를 구현해 만들어진다.
CoroutineContext 구성 요소는 보통 CoroutineContext.Key를 자신의 내부에 싱글톤 객체로 구현한다. 왜냐하면 1개의 키만 필요하기 때문이다.
CoroutineContext의 구성요소에 접근하기 위해서는 get 함수의 인자로 Key를 넘기면 된다.
참고로 get 함수는 연산자 함수로 선언되어 있어서 대괄호 ‘[]’ 로 대체 가능하다.
별로 안중요하기는 하지만.. CoroutineName, Job, CoroutineDispatcher, CoroutineExceptionHandler는 동반 객체이다. 따라서 CoroutineContext.Key를 구현하는 Key를 가져서 ‘.Key’를 쓰지 않고도 구성요소에 접근 가능하다.
CoroutineContext[CoroutineName.key] == CoroutineContext[CoroutineName]
fun main() = runBlocking<Unit> {
val coroutineContext = CoroutineName("MyCoroutine") + Dispatchers.IO
val nameFromContext = CoroutineContext[CoroutineName.key] // = getKey()
println(nameFromContext)
}


CoroutineDispatcher을 키로 사용해 구성요소에 접근하면, 실행은 가능하지만, 경고 메세지가 뜬다. CoroutineDispatcher.Key가 아직 Experimental API이기 때문이다.
OptIn 어노테이션을 붙여줘도 되지만, 안정적인 다른 API가 있다면 그것을 사용하는 것이 나을 것이다.

구성요소 인스턴스는 모두 key 프로퍼티를 가진다. 이를 사용해 싱글톤 Key에 접근할 수 있다.
요약하자면 CoroutineContext의 CoroutineDispatcher에 접근하기 위해서는, CoroutineContext[CoroutineDispatcher.key]를 사용해도 되지만 이는 아직 실험용이라 불안정하다.
따라서 직접 참조하지말고, 인스턴스(여기선 coroutineContext)에다가 .key를 붙여서 사용하면 된다.

CoroutineContext 객체에서 특정 구성 요소를 제거하기 위해서는 minusKey 함수를 호출하고, 구성요소의 키를 넘기면 된다.
minusKey()를 실행해도 객체가 아직 살아있는 것을 출력값을 통해 알 수 있다.

minusKey 함수 사용 시 minusKey 함수를 호출한 CoroutineContext 객체는 그대로 유지되고
구성 요소가 제거된 새로운 CoroutineContext 객체가 반환된다.
즉, 하나의 인스턴스를 삭제해도 결국 메모리에는 2개의 인스턴스가 있는 것이다.
• CoroutineContext 객체는 CoroutineName, CoroutineDispatcher, Job, CoroutineExceptionHandler 네가지 주요한 구성요소를 가진다
• CoroutineContext 객체는 구성요소에 대한 키-값 쌍으로 구성요소를 관리한다.
• CoroutineContext 객체에 구성 요소를 추가하기 위해서는 더하기 연산자(+)를 사용하면 된다.
• CoroutineContext 객체에 같은 구성 요소가 둘 이상 더해지면, 나중에 추가된 구성요소가 이전 값을 덮어씌운다.
• CoroutineContext의 구성요소에 접근하기 위해서는 get 함수의 인자로 구성요소의 Key를 넘기면 된다.
• Key 값은 구성요소 뒤에 .Key를 붙여 접근할 수 있다 예) CoroutineName.Key
• .Key를 생략할 수 있다. 예) CoroutineName.Key -> CoroutineName
• CoroutineContext의 minusKey 함수를 사용해 특정 구성요소를 제거할 수 있다.
• minusKey 함수 사용 시 minusKey 함수를 호출한 CoroutineContext 객체는 그대로 유지되고
구성 요소가 제거된 새로운 CoroutineContext 객체가 반환된다.
강의자료 출처 및 참고한 강의:
코틀린 코루틴 완전 정복 by 조세영님
https://www.inflearn.com/course/%EC%BD%94%ED%8B%80%EB%A6%B0-%EC%BD%94%EB%A3%A8%ED%8B%B4-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5/dashboard