[kotlin] Coroutine 입문 - 용어정리

조갱·2023년 4월 1일
1

Coroutine

목록 보기
5/9

kotlin + Coroutine을 하다 보면
정말 많은 용어들이 등장한다.
오늘은 용어들에 대한 개념을 정리하고자 한다.

suspend fun

kotlin에서 coroutine을 쉽게 사용할 수 있도록 도와주는 중단 함수이다.
이전 포스팅에서 공유하기를, 코루틴은 함수와의 상호작용으로 동작하는
협력함수라고 소개했다. suspend 키워드가 붙은 function은 잠시 중단하여
다른 함수가 실행될 수 있도록 한다.

suspend fun은 Coroutine Scope 내에서만 사용 가능하다.

시작하기 전

코루틴의 구조에 대해 먼저 알고가자.

CoroutineScope (Interface)

새로운 coroutine을 실행하기 위한 범위를 정의한다.
coroutineContext 를 상속하여, cancellation을 전파할 수 있다.
suspend fun은 이 범위 안에서만 실행이 가능하다.

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

CoroutineScope 인터페이스는 CoroutineContext 객체를 가지고있으며
관련 동작 (launch, async 등)이 실행될 때, 이 CoroutineContext를 바탕으로 실행하게 된다.

(아래 CoroutineContext에 대해 다시 소개하지만, 간략하게 설명하자면 코루틴이 실행되는 환경이라고 이해하면 된다.)

ContextScope

internal class ContextScope(context: CoroutineContext) : CoroutineScope {
    override val coroutineContext: CoroutineContext = context
    override fun toString(): String = "CoroutineScope(coroutineContext=$coroutineContext)"
}

CoroutineContext를 입력받아, 새로운 CoroutineScope를 생성한다.
internal class이기 때문에, 우리가 직접 호출하지 못하고
아래 fun CoroutineScope을 통해 생성된다.

CoroutineScope (Function)

public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
    ContextScope(if (context[Job] != null) context else context + Job())

CoroutineContext를 입력받아, 새로운 CoroutineScope를 만든다.

GlobalScope

public object GlobalScope : CoroutineScope {
    override val coroutineContext: CoroutineContext
        get() = EmptyCoroutineContext
}

EmptyCoroutineContext를 context로 사용하는 CoroutineScope를 반환한다.

상단에 GlobalScope 을 누르면 공식 문서에서도 볼 수 있겠지만, kotlin의 Coroutine 공식 문서에서는 GlobalScope를 아래와 같이 설명하고 있다.

Global scope is used to launch top-level coroutines which are operating on the whole application lifetime and are not cancelled prematurely.

즉, application 전체의 생명주기 동안 작동하며 조기에 취소되지 않는 top-level coroutine을 시작하는 데 사용된다.

EmptyCoroutineContext는 후술하겠지만, object 로 생성된 싱글톤 객체이다.
싱글톤 객체로 생성된다는 말은, 생명주기를 관리하는 주체가 개발자가 아닌 어플리케이션이라는 말이다.

또한, 본래 목적이 top-level에 사용되는 만큼, top-level이 아닌곳에서 사용한다면 Exception handling이나 cancellation 전파를 직접 구현해야 한다.

MainScope

@Suppress("FunctionName")
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

MainScope는 Android 에서 UI Component를 관리하기 위해 사용된다.
SupervisorJob() 의 자식 job들은 서로 독립적으로 fail이나 cancel 처리될 수 있다.

자식 job들의 fail이나 cancellation은 supervisor job이나 다른 자식들에게 영향을 미치지 않기 때문에 supervisor는 자식들의 failure의 핸들링에 대해 custom policy를 지정할 수 있다.

launch로 생성된 자식 job -> CoroutineExceptionHandler
async로 생성된 자식 job -> Deffered.await
을 통해 핸들링이 가능하다.

coroutineScope

public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R =
    suspendCoroutineUninterceptedOrReturn { uCont ->
        val coroutine = ScopeCoroutine(uCont.context, uCont)
        coroutine.startUndispatchedOrReturn(coroutine, block)
    }

위에 CoroutineScope(Function)과 헷갈리지 말자.
앞에 c가 여기는 소문자, 위에는 대문자이다.

coroutineScope는 바로 바깥에 있는(부모) CoroutineScope를 상속받으며,
부모를 일시 중단시키고 block 내의 작업을 수행한다.

Coroutine Builder

Coroutine Scope를 만들어주는 기능
패키지 : kotlinx.coroutines.Builders

runBlocking

expected (공용부)

public expect fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T

actual (구현부)

public actual fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T

일반적으로 runBlocking { ... } 의 정의를 따라가면, actual 구현부를 따라간다.
특이하게도, context를 받게 돼있지만 따로 context를 입력하지 않아도 사용할 수 있다.
이는 runBlocking의 공용부에 context는 EmptyCoroutineScope를 기본으로 사용하기 때문이다. 참조

runBlocking은 작업이 완료될 때까지 현재 스레드를 블로킹 시키기 때문에, Coroutine의 목적과는 맞지 않으며, 공식 문서에서도 사용을 지양하고 있다.

그럼에도 불구하고 존재하는 이유는, runBlocking은 본래 블로킹 형태로 제공되는 라이브러리와 연동을 위해 구현되었으며, main 함수와 test 코드에서 사용되도록 권장된다.

withContext

suspend fun <T> withContext(context: CoroutineContext, block: suspend CoroutineScope.() -> T): T

withContext는 아래와 같은 상황에서 사용된다.

  • CoroutineScope 내에서 다른 context로 전환이 필요할 때,
  • CoroutineScope 내에서 순서의 보장이 필요할 때

withContext 는 현재 CoroutineScope를 일시 중단하고 결과값을 반환하기 때문에 순서의 보장이 필요할 때 사용될 수 있다.

일시 중단하고 결과값을 반환한다는 점에서 runBlocking과 유사해 보이지만, runBlocking 은 현재 스레드를 차단 해버리며, withContext는 일시 중단한다는 점에서 차이가 크다.

async, launch

async

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

launch

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job
  • async
    • block 함수가 T를 반환
    • Deffered<T>를 반환
    • 즉, 비동기 작업의 결과값을 받을 수 있다.
  • launch
    • block 함수의 반환형이 없음 (Unit)
    • Job 객체를 반환
    • 결과값을 받지 못한다.

Deferred 인터페이스를 코드상에서 확인해보면, Job 인터페이스를 상속받는 것을 확인할 수 있다.

public interface Deferred<out T> : Job { ... }

Job에서 관리하는 상태는 6가지로, flow와 각 상태별 설명은 아래와 같다.

StateisActiveisCompletedisCancelled
New (optional initial state)FalseFalseFalse
Active (deault initial state)TrueFalseFalse
Completing (transient state)TrueFalseFalse
Cancelling (transient state)FalseFalseTrue
Cancelled (final state)FalseTrueTrue
Completed (final state)FalseTrueFalse

CoroutineContext

코루틴의 실행을 제어하고 관리하는 데 사용되며, Dispatcher와 Job을 조합하여 구성할 수 있다.

모든 코루틴은 CoroutineContext를 필요로 하며,
Dispatcher 와 Job은 CoroutineConetext를 상속받는다.

  • Job : 코루틴의 작업을 관리하는데 사용 (join(), cancel(), cancelAndJoin(), isActive, isCancelled, isCompleted...)

  • Dispatcher : 코루틴이 실행되는 스레드를 제어하는 데 사용 (Dispatchers.Default, Dispatchers.IO...)

Job

 public interface Job : CoroutineContext.Element
  public interface Element : CoroutineContext

Dispatcher

public abstract class CoroutineDispatcher :
    AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor
    
public abstract class AbstractCoroutineContextElement(public override val key: Key<*>) : Element

public interface Element : CoroutineContext

CoroutineDispatcher

Dispatcher는 코루틴이 실행되는 스레드를 제어하는 데 사용
(코루틴이 실행되는 스레드풀이라고 이해해도 될 듯 하다)

Dispatchers object 에서 기본적으로 제공하는 Dispatcher는 아래와 같다.

  • Default
  • Main
  • Unconfined
  • IO

https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/

여기에 각 종류별 설명이 있으니 참고하면 좋다!

Custom

코루틴에서는 Excutor 객체에 대해 coroutineDispatcher로 변환하는 확장함수를 제공한다.

public fun Executor.asCoroutineDispatcher(): CoroutineDispatcher

코루틴에서 기본적으로 제공하는 Dispatcher는 일반적으로 Mobile Device에 맞춰져있다.
Web Application (Spring) 개발 환경에서는 상황에 따라 다양한 커스텀이 필요할 수도 있다.
Spring Framework에서 제공하는 TreadPoolTaskExecutor 을 통해 스레드 풀을 customize하고, 필요한 옵션을 추가할 수도 있다.

Reference
코틀린 공식 문서
https://stackoverflow.com/questions/71272516/why-can-runblocking-be-invoked-without-providing-a-coroutinecontext-if-there
https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/
https://stackoverflow.com/questions/53535977/coroutines-runblocking-vs-coroutinescope/53536713#53536713

profile
A fast learner.

0개의 댓글