Android/학습

[Kotlin] Kotlin Coroutine with Dispatcher

한때미 2025. 4. 9. 17:10

코루틴 Coroutine

Android에서 비동기적으로 실행되는 코드를 단순화하는 데 사용할 수 있는 동시성(concurrency) 디자인 패턴이다.

 

 

동시성(Concurrency) vs 병렬성(Parallelism)

동시성과 병렬성의 시각으로 표시한 이미지

 

병렬성은 실제로 두 작업이 같이 처리되는 것을 뜻하고 동시성을 그 와는 다르게 두 작업이 번갈아가면서 실행되며 동시에 처리되는 것을 뜻한다. 실제로 A작업, B작업이 같이 처리되는 것이 아니라 A작업했다가 B작업했다가 돌아가면서 처리하는 것이다.

실제 한 시점을 보면 하나의 작업만 처리되는 것을 알 수 있다.

 

코루틴은 이 동시성에 대한 디자인 패턴(솔루션)인 것인데,

코루틴이 병렬성이 아닌 동시성 디자인 패턴인 건에 대해서는 코루틴의 동작 방식에 대해 이해하면 쉽다.

 

 

코루틴은 주로 경량 스레드라고 하는데 스레드를 전환하는 OS단에서 일어나는 Context Switching이 일어나지 않고 작업을 전환할 수 있기 때문이다.

또한, 코루틴을 실제로 실행시키는 것은 스레드인데 이 코루틴의 대표적인 특징은 다음과 같다. 

 

특징

코루틴은 Android에서 비동기 프로그래밍을 구현할 때 권장하는 솔루션이며,

공식 문서에서 제시하는 주목할 만한 기능은 다음과 같다.

  • 경량 : 코루틴이 실행되는 스레드를 차단하지 않는 suspension을 지원하여 단일 스레드에서 여러 코루틴을 실행할 수 있습니다. Suspending은 많은 동시 작업을 지원하면서 차단보다 메모리를 절약합니다.
  • 메모리 누수 감소 : 구조화된 동시성을 사용하여 범위 내에서 작업을 실행합니다.
  • 취소 지원 기능 내장 : 취소는 실행 중인 코루틴 계층을 통해 자동으로 전파됩니다.
  • Jetpack 통합 : 많은 Jetpack 라이브러리에는 전체 코루틴 지원을 제공하는 확장 기능이 포함되어 있습니다. 일부 라이브러리는 구조화된 동시성에 사용할 수 있는 자체 코루틴 범위도 제공합니다.

경량(Light-weight)

 

A coroutine is an instance of a suspendable computation.

 

코루틴이라는 건 멈출 수 있는(suspendable) 작업 단위가 있고,

그 작업 단위를 실행시키는 실제 객체(instance)가 바로 코루틴이다.

 

즉, 코루틴은 멈출 수 있는 작업 단위를 실제 실행시키는 객체이다.

 suspendable computation: 멈출 수 있는 작업 단위

 

여기서 멈출 수 있는 작업 단위는 말 그대로 코드 실행 중 '멈출(중단)'될 수 있으며, suspend fun으로 코루틴(실행 객체) 없이 개별로 만들어 코루틴에 전달해 줄 수도 있다.

 

참고로 스레드는 중단될 수 없다.

스레드(thread)는 블로킹(blocking) 될 수는 있어도, 코루틴처럼 중단(suspend) 되는 개념은 아니다.

 

개념 스레드(thread) 코루틴(coroutine)
중단(suspend) 가능 여부 불가능 가능
멈춤 방법 block(=스레드 점유한 채 멈춤) suspend(=스레드 반납 후 멈춤)
멈추면? CPU/메모리 계속 점유 스레드 반납 → 다른 작업 처리 가능
다시 실행 깨어나서 계속 실행 resume 통해 다시 이어서 실행

*** 물론, 코루틴도 runBlocking를 통해서 블로킹이 가능하긴 하다.

 

 

다음 예를 들어, 스레드의 blocking과 코루틴의 suspend의 차이를 간단하게 비교하면 다음과 같다.

fun doWork() {
    Thread.sleep(1000) // 블로킹
    println("작업 완료")
}

해당 스레드는 1초 동안 다른 작업을 실행시키지 못한다.

하지만 해당 스레드는 계속 CPU를 점유하고 있기 때문에 CPU 또한, 다른 곳에 활용되지 못하고 대기 상태로 멈춰 있다.

(멀티코어 환경에서는 다른 코어에서 다른 스레드가 실행될 수 있지만 해당 스레드가 점유한 곳은 계속 대기 상태..)

 

suspend fun doWork() {
    delay(1000) // 논블로킹
    println("작업 완료")
}

해당 로직을 실행시키는 코루틴은 delay()에서 현재 스레드를 반납하고 멈춘다(suspend).

그렇게 반납된 스레드는 다른 코루틴이 가져가 사용할 수 있는 것이다.

 

 

이러한 차이에서 볼 수 있듯이 스레드의 개수는 제한적으로 생성되지만(OS가 관리), 코루틴은 가볍고 개수 제한에 연연해하지 않고 생성할 수 있겠다는 것도 엿볼 수 있다.

 

 

 

코루틴은 특정 스레드에 묶여있지 않고, 한 스레드에서 실행이 일시 중단되었다가 다른 스레드에서 다시 시작될 수도 있다.

코루틴은 어떻게 보면 스레드의 작은 작업 단위인 셈이다.

 

실제로 코루틴에 디스페쳐(Dispatchers)를 지정하면 디스페쳐가 해당하는 스레드 풀(thread pool)에 작업을 넣어두고, 해당 pool에 해당하는 스레드들이 하나씩 꺼내 실행시키는 구조이다.

 

대충 표현하면 이런 느낌

 


 

Coroutines always execute in some context represented by a value of the CoroutineContext type, defined in the Kotlin standard library.

 

코루틴은 코루틴의 생명주기 전체에 걸쳐 유지되는 Context를 가지고 있는데, 이 정보는 코루틴이 실행될 때 항상 가지고 있으며,

해당 Context는 (Kotlin 표준 라이브러리에 정의된) CoroutineContext이다.

 

 

CoroutineContext

map과 set 구조를 혼합하여 key-value의 index 세트 구조(Element instances)로 코루틴이 실행되는데 중요한 정보를 가지고 있다.

쉽게 말하면 key-value 형태로 주요 정보를 저장하는데, 간단하게 대표적인 값은 Dispatcher와 Job에 대한 정보이다.

CoroutineContext는 다양한 요소(코루틴을 제어하는 주요한 정보)의 집합인 셈이다.

Job SupervisorJob, Job 코루틴의 생명주기 관리 (취소, 완료 등)
Dispatcher Dispatchers.IO, Dispatchers.Main 어떤 스레드에서 실행할지 결정
CoroutineName CoroutineName("UserFetch") 디버깅용 네임 태그
기타 Context 요소 MDC, User-defined 사용자 정의 데이터 저장

 

 

만약 예를 들어 코루틴이 해당 정보를 모르거나, 생애 주기 전체에 해당 정보가 유지되지 못한다면, 다음과 같은 사항이 발생할 수 있다.

  • 어떤 스레드에서 동작하는지 모름
  • 부모 Job이 뭔지 모름
  • 취소 처리 어떻게 해야 할지 모름
  • 로깅이나 디버깅용 네임 태그 없음

 

코루틴은 이 CoroutineContext 정보를 통해서, 스레드에서 스레드에 의해 실행되지만 스레드 기반이 아닌, 자체 Context 기반으로 동작할 수 있게 되는 것이다.

 

Coroutines always execute in some context represented

 

풀어서 이야기하면, 실제로는 스레드에서 실행되지만, 실행환경 정보는 CoroutineContext가 가지고 있으며, 모든 코루틴은 CoroutineContext라는 실행 환경 정보에 의해서 실행된다는 뜻이다.

 


 

Dispatchers

코루틴 디스패처를 사용하면 해당 코루틴 실행을 특정 스레드로 제한하거나, 스레드 풀로 디스패치하거나 제한 없이 실행되도록 할 수 있다.

 

launch 및 async와 같은 모든 코루틴 빌더는 선택적(Optional, nullable)인 CoroutineContext 매개변수를 받을 수 있으며,

이를 통해 어떤 디스패쳐(Dispatcher)를 실행할지 명시할 수 있으며, 그 외에도 그 외에도 Job, CoroutineName, ExceptionHandler 같은 Context Element 들도 함께 넣어서 명시할 수 있다.

 

※ launch는 코루틴 빌더이며 코루틴을 실행시킨다.

 

 

쉽게 예시를 들면 이렇게 값을 넘겨줄 수 있다.

CoroutineContext가 아닌 Dispatcher에 대한 정보만 달랑 넘겨주는 느낌이지만, CoroutineContext 설계 방식에 따라 이런 식으로 값을 넘겨줄 수 있게 구현되어 있다.

launch(Dispatchers.Default) { // will get dispatched to DefaultDispatcher 
    println("Default: I'm working in thread ${Thread.currentThread().name}")
}

 

실행 결과 : Default: I'm working in thread DefaultDispatcher-worker-1

 

대충 이런 방식으로 코루틴 빌더한테 원하는 정보를 CoroutineContext 형태로 전달해 줄 수 있다.

CoroutineContext = 
    Dispatcher (스레드 실행 위치) +
    Job (생명주기 관리) +
    CoroutineName (디버깅 용 이름) +
    CoroutineExceptionHandler (예외 핸들링)

 

실제로 디스페쳐는 CoroutineContext.Element를 상속받아 구현된다.

 


Dispatchers의 종류

 

Dispatchers.Main

Android의 UI와 상호작용하는 유일한 메인 스레드에서 실행되며,

그렇기 때문에 UI와 상호 작용하고 빠른 작업을 수행하는 데만 사용해야 한다.

예로는 Android UI 프레임워크 작업 실행, LiveData객체 업데이트나 가벼운 suspend 함수를 호출할 수 있다.

 

메인 스레드는 blocking 될 경우, ANR를 발생시키는 특이점을 가지고 있다.

 

 

Dispatchers.IO

이 디스패처는 메인 스레드 외부에서 디스크 또는 네트워크 I/O를 수행하도록 최적화되어 있다.

예를 들어 Room 구성 요소 사용, 파일 읽기/쓰기, 네트워크 작업 실행 등이 있다.

 

또한, IO Dispatcher의 한계는 64개 스레드이며, 이때 + ⍺가 될 수 있는데 ⍺는 Dispatcher.Default의 한계만큼이며, 이는 CPU 코어(core) 개수(size) 만큼이다.

 

Dispatcher.IO와 Dispatcher.Default는 같은 thread pool을 공유한다.

 

 

Dispatchers.Default

이 디스패처는 메인 스레드 외부에서 CPU 집약적 작업을 수행하도록 최적화되어 있다.

예시 사용 사례로는 목록 정렬 및 JSON 구문 분석이 있다.

 

 

여기서 Dispatcher.IO와 Dispatcher.Default의 차이를 짚고 간다면,

Dispatcher.IO는 네트워크 입출력 같은 대기시간이 있는 작업에 적합하며,

여러 스레드가 대기/활성/대기를 반복하며 효율적으로 사용하기 위함 (64개의 넉넉함)

 

Dispatcher.Default의 경우 대기시간이 없고 지속적으로 CPU의 작업을 필요로 하는 작업에 적합하다.

그렇기 때문에 코어(core) 수만큼의 스레드를 생성하여 CPU를 점유해 최대의 효율로 처리하기 위함으로 보임.

 

 

Dispatchers.Unconfined

해당 디스페쳐는 비제한 디스패쳐로, 특정 디스페쳐를 지정하지 않겠다는 의미이다.

 

코루틴 디스패처는 호출자 스레드에서 코루틴을 시작하지만 첫 번째 정지 지점까지만 동작하고,

정지 지점 이후 코루틴이 재개될 때에는 실행되는 스레드가 첫 번째 시작 스레드와는 다른 종류의 스레드 일 수도 있다.

 

Unconfined 디스패처는 CPU 시간을 소모하지 않고 특정 스레드에 제한된 공유 데이터(예: UI)를 업데이트하지 않는 코루틴에 적합하다.

 

실제로는 어떤 스레드에서 동작하는지 알기 어렵기 때문에 잘 사용하지 않는 디스페쳐라고 한다.

뭐.. 빠르게 완료되어야 할, 위의 상황 하고도 일치하지 않는 경우 사용하기도 한다고 한다.

 

 

 

기본적으로 디스패처는 외부 CoroutineScope에서 상속된다.

 

추가로 정리해 보면 좋을 것들

  • Coroutine 부모-자식 관계
  • cancel, error 전파
  • Coroutine scope
  • Coroutine job 및 생명주기
  • Dispatcher 커스텀

참고 자료

 

https://developer.android.com/kotlin/coroutines?hl=ko

 

Android의 Kotlin 코루틴  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Android의 Kotlin 코루틴 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 코루틴은 비동기적으로 실행되는

developer.android.com

 

https://kotlinlang.org/docs/coroutine-context-and-dispatchers.html

 

Coroutine context and dispatchers | Kotlin

 

kotlinlang.org

https://github.com/Kotlin/kotlinx.coroutines/issues/2272

 

IO Dispatcher creates more than 64 threads · Issue #2272 · Kotlin/kotlinx.coroutines

Annotation describes IO Dispatcher like this, It defaults to the limit of 64 threads or the number of cores (whichever is larger). As I understood, Maximum number of threads that can be created by ...

github.com