코루틴 공식 가이드 읽고 분석하기— Part 1 — Dive1

Myungpyo Shim
7 min readJan 15, 2019

CoroutineContext 와 CoroutineScope란 무엇인가?

Korean [English]

(목차로 돌아가기)

처음에 우리는 코루틴을 생성하기 위해서 GlobalScope.launch { … } 블록을 이용하여 코드를 작성했고 launch 중단함수를 코루틴 빌더라고 이야기 했습니다. 이름만 가지고 유추해보면 가장 넓은 스코프(GlobalScope = Application scope)를 갖는 코루틴을 만들 수 있도록 도와주는 빌더 같은데 실제로 무슨일이 일어나고 있는걸까요?

코루틴을 이해하기 위해서는 코루틴을 구성하는 두가지 주된 요소인CoroutineContext 와 CoroutineScope 에 대해 먼저 이해해야 합니다. 이를 위해 이들 각각의 구현 코드를 먼저 살펴보겠습니다.

위 코드는 CoroutineContext.kt 파일에 있는 코드 입니다. CoroutineContext 는 4가지 메서드를 가지고 있는데 그 역할은 다음과 같습니다.

  • get() : 연산자(operator) 함수로써 주어진 key 에 해당하는 컨텍스트 요소를 반환합니다.
  • fold() : 초기값(initialValue)을 시작으로 제공된 병합 함수를 이용하여 대상 컨텍스트 요소들을 병합한 후 결과를 반환합니다.
    예를들어 초기값을 0을 주고 특정 컨텍스트 요소들만 찾는 병합 함수(filter 역할)를 주면 찾은 개수를 반환할 수 있고, 초기값을 EmptyCoroutineContext 를 주고 특정 컨텍스트 요소들만 찾아 추가하는 함수를 주면 해당 요소들만드로 구성된 코루틴 컨텍스트를 만들 수 있습니다.
  • plus() : 현재 컨텍스트와 파라미터로 주어진 다른 컨텍스트가 갖는 요소들을 모두 포함하는 컨텍스트를 반환합니다. 현재 컨텍스트 요소 중 파라미터로 주어진 요소에 이미 존재하는 요소(중복)는 버려집니다.
  • minusKey() : 현재 컨텍스트에서 주어진 키를 갖는 요소들을 제외한 새로운 컨텍스트를 반환합니다.

그리고 Key 에 대한 인터페이스 정의가 있는데 Key 는 Element 타입을 제네릭 타입으로 가져야 합니다. Element 는 CoroutineContext 를 상속하며 앞서 이야기 한 key 를 멤버 속성으로 갖습니다. CoroutineContext 를 구성하는 Element 들의 예를 들어보면 CoroutineId, CoroutineName, CoroutineDispatcher, ContinuationInterceptor, CoroutineExceptionHandler 등을 들 수 있습니다. 이런 요소(element)들은 각각의 key 를 기반으로 CoroutineContext 에 등록 됩니다.

요약하자면 코루틴 컨텍스트(CoroutineContext)에는 코루틴 컨텍스트를 상속한 요소(Element) 들이 등록될 수 있고, 각 요소들이 등록 될때는 요소의 고유한 키를 기반으로 등록된다는 것입니다.

CoroutineContext 는 인터페이스로써 이를 구현한 구현체로는 다음과같은 3가지 종류가 있습니다.

  • EmptyCoroutineContext: 특별히 컨텍스트가 명시되지 않을 경우 이 singleton 객체가 사용됩니다.
  • CombinedContext: 두개 이상의 컨텍스트가 명시되면 컨텍스트 간 연결을 위한 컨테이너역할을 하는 컨텍스트 입니다.
  • Element: 컨텍트스의 각 요소들도 CoroutineContext 를 구현합니다.

위 이미지는 우리가 GlobalScope.launch{} 를 수행할 때 launch 함수의 첫번째 파라미터인 CoroutineContext 에 어떤 값을 넘기는지에 따라서 변화되어 가는 코루틴 컨텍스트의 상태를 보여줍니다.

각각의 요소를 + 연산자를 이용해 연결하고 있는데 이는 앞서 설명한 것처럼CoroutineContext 가 plus 연산자를 구현하고 있기 때문입니다. Element + Element + … 는 결국 하나로 병합 된 CoroutineContext (e.g. CombinedContext)를 만들어냅니다.

주황색 테두리는 CombinedContext 로서 CoroutineContext 와 Element 를 묶어 하나의 CoroutineContext 가 되는 개념입니다. 그리고 내부 코드에서 ContinuationInterceptor 는 이 병합 작업이 일어날 때 항상 마지막에 위치하도록 고정되는데 이는 인터셉터로의 빠른 접근을 위해서라고 커멘트 되어 있습니다.

자, 여기까지 CoroutineContext에 대해서 알아보았습니다. 그렇다면 CoroutineScope 란 무엇일까요?

CoroutineScope 는 기본적으로 CoroutineContext 하나만 멤버 속성으로 정의하고 있는 인터페이스 입니다.

우리가 사용하는 모든 코루틴 빌더들(예> 코루틴 빌더- launch, async -, 스코프 빌더- coroutineScope, withContext - 등등)은 CoroutineScope 의 확장 함수로 정의 됩니다. 다시말해, 이 빌더들은 CoroutineScope의 함수들인 것이고 이들이 코루틴을 생성할 때는 소속된 CoroutineScope 에 정의 된 CoroutineContext 를 기반으로 필요한 코루틴들을 생성해 내게 됩니다.

CoroutineScope.kt 파일에서는 (Android 가 현재 제 1의 지원 대상이라서 그런지… )사용자 정의 CoroutineScope의 한 예로 Android Activity 에서의 사용을 보여주고 있습니다. Activity 는 안드로이드 프레임워크에서 생명주기를 갖는 컴포넌트 중 하나이므로 스코프 정의를 보여주는 예제로 적합하다고 생각합니다.

이것은 안드로이드 액티비티 클래스 입니다. 클래스를 보면 CoroutineScope 인터페이스를 구현하고 있습니다. 그렇기 때문에 CoroutineScope 이 정의하는 CoroutineContext 멤버 속성을 구현해야 합니다.

여기서는 CoroutineScope 의 CoroutineContext 를 Dispatchers.Main + job 으로 정의함으로써, Activity에서 생성되는 코루틴은 메인스레드(UI Thread)로 디스패치 되고. 액티비티에서 정의한 Job 객체를 parent 로 하는 Job 들을 생성함으로써 액티비티 Job 과 그 운명을 같이하게 됩니다. (cancel parent = cancel all children)

이제 CoroutineScope 가 무엇인지는 알겠는데 지금까지 예제에서 사용한 GlobalScope.launch {}코루틴 빌더는 정확히 무엇일까요? CoroutineScope 은 지금까지 본것과 같이 인터페이스일 뿐입니다. 실제 구현체는 위에 예로 든 Activity와 같이 어떠한 생명주기를 갖는 오브젝트에 적용하여 사용자 정의 스코프를 만들 수도 있지만, 편의를 위해서 코루틴 프레임워크에 미리 정의된 스코프들도 있습니다. 그 중 하나가 GlobalScope 이며 구현은 다음과 같습니다.

GlobalScope 는 Singleton object 로써 EmptyCoroutineContext 를 그 컨텍스트로 가지고 있습니다. EmptyCoroutineContext 는 구현해야할 모든 CoroutineContext 멤버 함수들에 대해서 기본 구현만 정의한 컨텍스트 입니다. 이 기본 컨텍스트는 어떤 생명주기에 바인딩 된 Job 이 정의되어 있지 않기 때문에 애플리케이션 프로세스와 동일한 생명주기를 갖게 됩니다.

다시 말해 GlobalScope.launch{} 로 실행한 코루틴은 애플리케이션이 종료되지 않는 한 필요한 만큼 실행을 계속해 나갑니다.

끝.

(목차로 돌아가기)

--

--