관리 메뉴

나만을 위한 블로그

[Kotlin] 코루틴 디스패처 본문

Android

[Kotlin] 코루틴 디스패처

참깨빵위에참깨빵_ 2025. 5. 3. 19:15
728x90
반응형

코루틴을 구현할 때 launch, async, withContext를 사용할 수 있다. 이 때 코루틴 디스패처(이하 디스패처)를 써서 코루틴이 어떤 쓰레드 풀에서 작동하게 할 지를 정할 수 있다.

아래는 코틀린 공식문서 중 디스패처를 설명하는 문서다.

 

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

 

Coroutine context and dispatchers | Kotlin

 

kotlinlang.org

코루틴은 항상 코틀린 표준 라이브러리에 정의된 코루틴 컨텍스트 타입의 값으로 표시되는 컨텍스트에서 실행된다
코루틴 컨텍스트는 여러 요소의 집합이다. 주요 요소는 코루틴의 Job, 디스패처다

< 디스패처와 쓰레드 >

코루틴 컨텍스트에는 해당 코루틴이 실행에 사용하는 쓰레드를 결정하는 코루틴 디스패처가 포함된다. 코루틴 디스패처는 코루틴 실행을 특정 쓰레드로 제한하거나 쓰레드 풀로 디스패치하거나 제한되지 않은 상태로 실행할 수 있다
launch, async 같은 모든 코루틴 빌더는 새 코루틴 및 기타 컨텍스트 요소에 대한 디스패처를 명시적으로 지정하는 데 쓸 수 있는 선택적 코루틴 컨텍스트 매개변수(CoroutineContext)를 허용한다
import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {

    launch { // context of the parent, main runBlocking coroutine
        println("main runBlocking      : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(Dispatchers.Unconfined) { // not confined -- will work with main thread
        println("Unconfined            : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(Dispatchers.Default) { // will get dispatched to DefaultDispatcher 
        println("Default               : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(newSingleThreadContext("MyOwnThread")) { // will get its own new thread
        println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
    }
    
}

// >> Unconfined            : I'm working in thread main @coroutine#3
// >> Default               : I'm working in thread DefaultDispatcher-worker-1 @coroutine#4
// >> main runBlocking      : I'm working in thread main @coroutine#2
// >> newSingleThreadContext: I'm working in thread MyOwnThread @coroutine#5
로그 순서는 실행할 때마다 달라질 수 있다. launch를 매개변수 없이 쓰면 실행 중인 코루틴 스코프에서 컨텍스트(따라서 디스패처)를 상속받는다. 이 때 메인 쓰레드에서 실행되는 runBlocking 코루틴의 컨텍스트를 상속한다
Dispatchers.Unconfined는 메인 쓰레드에서 실행되는 것처럼 보이지만 실제론 다른 메커니즘을 가진 특수 디스패처다. Main 디스패처는 스코프에 다른 디스패처가 명시적으로 지정되지 않은 때 사용된다. Default 디스패처로 표시되며 공유 백그라운드 쓰레드 풀을 사용한다
newSingleThreadContext는 코루틴을 실행할 새 쓰레드를 만든다. 전용 쓰레드는 매우 비싼 리소스다. 실제 앱에선 더 이상 불필요할 때 close 함수를 써서 해제하거나 최상위(top-level) 변수에 저장해서 앱 전체에서 재사용해야 한다

 

공식문서에 따르면 디스패처는 코루틴이 실행될 쓰레드를 결정하는 요소라고 할 수 있다. 종류는 Main, Unconfined, Default, IO, newSingleThreadContext가 있다. 참고로 newSingleThreadContext는 리턴 타입이 CloseableCoroutineDispatcher인데 엄연히 디스패처를 리턴하는 함수고, 공식문서에서도 CloseableCoroutineDispatcher를 코루틴 디스패처라고 써 놨기 때문에 newSingleThreadContext는 디스패처에 속한다.

 

https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-closeable-coroutine-dispatcher/

 

CloseableCoroutineDispatcher | kotlinx.coroutines – Kotlin Programming Language

expect abstract override fun close() Initiate the closing sequence of the coroutine dispatcher. After a successful call to close, no new tasks will be accepted to be dispatched. The previously-submitted tasks will still be run, but close is not guaranteed

kotlinlang.org

 

newSingleThreadContext는 코루틴을 실행하기 위한 새 쓰레드를 만드는데 쓰레드를 만드는 건 매우 비용이 높은 작업이다. 함부로 쓰는 것은 지양해야 하고, 사용해야 한다면 반드시 close()를 호출해서 해제하거나 최상위 변수에 저장하고 앱 전반적으로 사용해야 한다.

close() 호출을 까먹는 것이 걱정된다면 use 확장 함수를 사용하면 된다. use 확장 함수는 예외가 발생하든 하지 않든 close() 호출이 보장되는 함수기 때문이다.

use가 뭔지 모른다면 아래 포스팅을 참고하고, 책으로 보고 싶다면 이펙티브 코틀린의 아이템 9를 확인하면 된다.

 

https://onlyfor-me-blog.tistory.com/771

 

[Kotlin] use 확장 함수 알아보기

예전에 이펙티브 코틀린을 읽으면서 use를 써서 리소스를 닫으라는 내용을 봤었다. https://onlyfor-me-blog.tistory.com/489 [이펙티브 코틀린] 아이템 9. use를 써서 리소스를 닫아라 더 이상 필요하지 않을

onlyfor-me-blog.tistory.com

 

Unconfined 디스패처는 caller thread에서 코루틴을 시작하지만 1번째 일시 중단 지점까지만 코루틴을 시작한다. 일시 중단 후에는 호출된 일시 중단 함수에 의해 완전히 결정된 쓰레드에서 코루틴을 재개한다. Unconfined 디스패처는 CPU 시간을 소비하지 않거나 특정 쓰레드에 국한된 공유 데이터(UI 등)를 업데이트하지 않는 코루틴에 적합하다
반면에 디스패처는 기본적으로 외부 코루틴 스코프에서 상속된다. 특히 runBlocking 코루틴의 기본 디스패처는 caller thread에 한정돼 있으므로 이걸 상속하면 예측 가능한 FIFO(선입선출) 스케줄링으로 실행을 이 쓰레드로 한정하는 효과가 있다
import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {

    launch(Dispatchers.Unconfined) { // not confined -- will work with main thread
        println("Unconfined      : I'm working in thread ${Thread.currentThread().name}")
        delay(500)
        println("Unconfined      : After delay in thread ${Thread.currentThread().name}")
    }
    launch { // context of the parent, main runBlocking coroutine
        println("main runBlocking: I'm working in thread ${Thread.currentThread().name}")
        delay(1000)
        println("main runBlocking: After delay in thread ${Thread.currentThread().name}")
    }
    
}

// >> Unconfined      : I'm working in thread main @coroutine#2
// >> main runBlocking: I'm working in thread main @coroutine#3
// >> Unconfined      : After delay in thread kotlinx.coroutines.DefaultExecutor @coroutine#2
// >> main runBlocking: After delay in thread main @coroutine#3
따라서 runBlocking에서 컨텍스트가 상속된 코루틴은 메인 쓰레드에서 계속 실행되고 컨텍스트가 상속되지 않은 코루틴은 delay()가 사용 중인 메인 쓰레드에서 재개된다

Unconfined 디스패처는 코루틴의 일부 연산을 즉시 수행해야 해서 나중에 실행하기 위해 코루틴을 디스패치할 필요가 없거나 사이드 이펙트가 발생하는 특정 코너 케이스에 유용하게 쓸 수 있는 고급 메커니즘이다. Unconfined 디스패처는 일반 코드에서 써선 안 된다...(중략)

 

다른 디스패처는 별 말이 없는데 Unconfined 디스패처에 대해선 말하는 내용이 많다.

결론은 마지막에 표시한 것처럼 일반적인 코드에서 사용하면 안 된다는 것이다. 예제 코드에서도 볼 수 있듯 처음엔 main 쓰레드에서 coroutine#2란 이름을 갖고 시작하지만 delay() 후에는 갑자기 coroutines.DefaultExecutor 쓰레드라는 다른 쓰레드에서 재개되는 걸 볼 수 있다. 즉 일시 중단 지점 전후로 실행되는 쓰레드가 달라진다. Unconfined를 Default, IO로 변경하면 같은 쓰레드 풀에서 동작하는 걸 볼 수 있다.

 

이것은 Unconfined 디스패처를 쓰면 일시 중단된 코루틴을 재개할 때 코루틴이 어디로 튀어서 어느 쓰레드에서 실행될지 모른다는 뜻이다. 그러면 뒷감당이 매우 힘들어질 것이다. 공식문서에서 권장하는 것처럼 Unconfined는 사용하지 않는 게 정신건강에 좋을 것이다.

 

Dispatchers.Main

 

이제 디스패처 하나씩 확인해 본다. 먼저 메인 디스패처인데 이름에서 알 수 있듯 메인 쓰레드에서 코루틴을 실행하게 하는 디스패처다.

 

https://developer.android.com/kotlin/coroutines/coroutines-adv#main-safety

 

Kotlin 코루틴으로 앱 성능 향상  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Kotlin 코루틴으로 앱 성능 향상 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Kotlin 코루틴 명확하고

developer.android.com

이 디스패처를 써서 안드로이드 메인 쓰레드에서 코루틴을 실행한다. UI와 상호작용하고 빠른 작업을 수행할 때만 써야 한다. 예를 들어 suspend 함수 호출, 안드로이드 UI 프레임워크 작업 실행, LiveData 객체 업데이트 등이 있다

 

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

 

Main | kotlinx.coroutines – Kotlin Programming Language

Main commonjsAndWasmSharedjvmnative A coroutine dispatcher that is confined to the Main thread operating with UI objects. Usually such dispatchers are single-threaded. Access to this property may throw an IllegalStateException if no main dispatchers are pr

kotlinlang.org

UI 객체로 작동하는 메인 쓰레드에 국한된 코루틴 디스패처. 일반적으로 이런 디스패처는 싱글 쓰레드다
클래스 경로에 메인 디스패처가 없을 때 이 프로퍼티에 접근하면 IllegalStateException이 발생할 수 있다...(중략)

 

메인 쓰레드에서 코루틴을 실행하지만 그렇기 때문에 가벼운 작업을 코루틴으로 돌릴 때만 써야 한다. 안드로이드 UI 프레임워크 작업 실행을 비롯해 웹뷰의 evaluateJavascript() 호출도 메인 디스패처에서 수행할 수 있다.

 

Dispatchers.IO

 

https://developer.android.com/kotlin/coroutines/coroutines-adv#main-safety

 

Kotlin 코루틴으로 앱 성능 향상  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Kotlin 코루틴으로 앱 성능 향상 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Kotlin 코루틴 명확하고

developer.android.com

메인 쓰레드 밖에서 디스크 or 네트워크 IO를 수행하도록 최적화돼 있다. Room 컴포넌트 사용, 파일 읽고 쓰기, 네트워크 작업 실행 등이 있다

 

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

 

IO | kotlinx.coroutines – Kotlin Programming Language

The CoroutineDispatcher that is designed for offloading blocking IO tasks to a shared pool of threads. Additional threads in this pool are created and are shutdown on demand. The number of threads used by tasks in this dispatcher is limited by the value of

kotlinlang.org

블로킹 IO 작업을 공유 쓰레드 풀로 오프로드하게 설계된 코루틴 디스패처. 이 풀에 추가 쓰레드가 생성되고 필요에 따라 종료된다. 이 디스패처의 작업에서 사용하는 쓰레드 수는 기본 64개 쓰레드 또는 코어 개수 중 큰 값으로 제한된다...(중략)

 

네트워크 통신, 파일 읽고 쓰기 작업을 할 때 쓰이는 디스패처다. 클린 아키텍처로 안드로이드 앱을 만든다면 뷰모델 또는 usecase에서 매개변수로 디스패처를 받게 만든 다음, 기본값으로 IO 디스패처를 설정하고 필요에 따라 다른 디스패처를 넘겨 그 디스패처에서 함수가 실행되게 할 수 있다.

추가로 뷰모델 함수의 viewModelScope.launch 뒤에 IO 디스패처를 명시적으로 붙이는 경우가 있는데, 이것은 상황에 따라 괜찮을 수 있지만 장기적으로 보면 안 좋은 선택일 수 있다. 이유는 아래와 같다.

 

  • 테스트 어려움 : 단위 테스트를 작성하고 싶다면 뷰모델에 하드코딩한 디스패처 때문에 가짜 디스패처로 주입하는 TestDispatcher와 충돌을 일으킬 여지가 있다. 디벨로퍼에서도 디스패처를 하드코딩하지 않을 걸 권장하고 있다
  • 관심사 분리를 위반함 : 디스패처는 코루틴을 어떤 쓰레드에서 실행할지에 대한 일종의 정책이고 뷰모델은 뭘 할지다. 서로 다른 관심사를 갖기 때문에 뷰모델에 디스패처를 하드코딩하는 건 관심사 분리를 위반하는 것으로 볼 수 있다

 

그래서 앞서 말한 대로 뷰모델 밖에 있는 usecase에서 설정하거나 또는 디스패처 주입만 담당하는 DispatcherManager, DispatcherProvider 같은 인터페이스 + 구현 클래스의 조합을 만들고 디스패러를 주입받는 방법을 사용한다.

토이 프로젝트거나 소규모 프로젝트라면 어떨지 몰라도 미래의 유지보수성을 고민해야 하는 프로젝트라면 번거로워도 디스패처를 주입하는 식으로 짜는 것이 바람직하다.

그러나 난 다 모르겠고 IO를 쓰겠다면 아래 케이스에 해당하는지 따져본다.

 

  • 앱 구조가 단순하다
  • 테스트 안 짤 거다
  • 누가 봐도 이 함수는 IO 작업 하는 함수다

 

이런 경우라면 허용할 수 있겠지만 점점 복잡한 구조가 되면 언젠가 리팩토링해야 하는 시기가 올 것이다.

항상 선택과 그로 인해 지는 책임은 본인 몫이다.

 

그러나 IO 디스패처는 Default 디스패처와 같이 사용하면 좋지 않을 수 있다는 의심을 받는 디스패처다. 이 내용은 Default 디스패처를 본 후에 확인한다.

 

Dispatchers.Default

 

https://developer.android.com/kotlin/coroutines/coroutines-adv#main-safety

 

Kotlin 코루틴으로 앱 성능 향상  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Kotlin 코루틴으로 앱 성능 향상 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Kotlin 코루틴 명확하고

developer.android.com

메인 쓰레드 밖에서 CPU 집약적 작업을 수행하도록 최적화되어 있다. 사용 사례는 리스트 정렬, JSON 파싱이 있다

 

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

 

Default | kotlinx.coroutines – Kotlin Programming Language

Default

kotlinlang.org

디스패처나 다른 ContinuationInterceptor가 컨텍스트에 지정되지 않은 경우 launch, async 같은 모든 표준 빌더에서 쓰이는 기본 코루틴 디스패처
JVM과 네이티브의 공유 쓰레드 풀에 의해 지원된다. 이 디스패처가 쓰는 최대 쓰레드 개수는 기본적으로 CPU 코어 수와 같지만 최소 2개다

 

리스트 정렬, JSON 파싱을 위해 권장되는 디스패처다. 그러나 이 디스패처가 CPU 집약적인 작업에 정말로 좋은 디스패처인가? 다른 Main이나 IO 디스패처에선 리스트 정렬, JSON 파싱이 왜 권장되지 않는가?

아래는 두 디스패처에 대해 의구심을 갖는 포스팅이다. 일부만 가져왔다.

 

https://www.techyourchance.com/coroutines-dispatchers-default-and-dispatchers-io-considered-harmful/?source=post_page-----7d633d1cac6a---------------------------------------

 

Coroutines Dispatchers.Default and Dispatchers.IO Considered Harmful

My argument against Kotlin Coroutines' default background dispatchers: Dispatchers.Default and Dispatchers.IO.

www.techyourchance.com

(중략)...공식 문서에선 백그라운드 작업으로 Default, IO 디스패처를 권장하며 수많은 게시물에서 이 권장 사항을 반복하고 있고 이런 디스패처는 수 년 동안 많은 프로젝트에서 쓰여 왔다. 이렇게 널리 퍼진 걸 안티 패턴이라고 부르는 건 대담한 표현이다
한때 안드로이드 생태계에서 가장 핫한 이슈였던 동시성 프레임워크인 RxJava도 비슷한 전략을 쓴다. 이 프레임워크는 한 쌍의 스케줄러 객체, Default 및 IO 디스패처와 프로퍼티, usecase가 유사하다. 따라서 난 코루틴 프레임워크의 Default 디스패처가 안티 패턴이라고 주장함으로써 RxJava의 표준 백그라운드 스케줄러도 안티 패턴이라고 주장한다...(중략)

문서에선 Default 디스패처의 사용 사례에 대한 설명이 없다. 실제로 코틀린 공식문서 어디서도 사용 지침을 찾을 수 없다. 따라서 내가 볼 수 있는 한 CPU 집약적 작업에 Default 디스패처를 써야 한다는 주장에 대한 공식 근거는 없다. 그것만으로도 큰 위험 신호다
또한 이 디스패처가 CPU 코어가 1개인 디바이스에선 2개의 쓰레드로 지원되지만 그렇지 않은 경우 쓰레드 수가 CPU 코어 수와 같은 이유는 뭔가? 쓰레드가 하나만 있으면 문제 있는 것 같지만 이에 대한 정보를 찾을 수 없다.
IO 디스패처의 쓰레드 개수 선택에도 이상한 점이 있다. 따라서 Default 디스패처의 설명할 수 없는 코너 케이스는 전체적으로 뭔가 잘못됐다는 인상을 준다...(중략)

IO 디스패처는 JVM 전용이며 문서에 따르면 블로킹 IO 작업을 공유 쓰레드 풀로 오프로드하도록 설계됐다고 한다. IO 디스패처는 기본적으로 64개의 쓰레드, 즉 코어 수로 제한된다. 그럼 왜 이 특정 숫자가 필요한지 의문이 생긴다. 또한 서로 다른 JVM 대상(안드로이드 vs 백엔드 등)의 IO 로드 프로파일에 큰 차이가 있단 걸 고려할 때 이 숫자가 한 대상에 최적일지라도 다른 환경에선 매우 부적합할 수 있다는 건 분명하다. 이 중요한 사실에 대한 언급이 전혀 없다
또한 문서에선 IO 디스패처에서 쓰레드 개수를 바꿀 수 있다고 하지만 언제 이 작업을 해야 하는지에 대한 내용은 없다. 이상하게도 쓰레드 수를 제한하는 2가지 메커니즘이 존재한다
마지막으로 2번째 임계값보다 더 많은 쓰레드가 필요한 경우 커스텀 디스패처를 쓰라는 권장사항이 있다. 이 디스패처가 적어도 어떤 경우엔 나쁜 방법이란 게 분명하지만 문서에선 그 이유를 설명하지 않는다
따라서 IO 디스패처의 맥락에서 중요한 질문은 아래와 같다

- 64개의 쓰레드 제한이 이 라이브러리 유저에게 정확히 어떤 장점이 있는가?
- IO 디스패처의 size와 관련된 장단점은 뭔가?
- backing 쓰레드 풀의 크기를 제한하는 2가지 메커니즘이 있는 이유는 뭔가?
- 더 많은 쓰레드가 필요한 경우 커스텀 디스패처로 전환하면 어떤 장점이 있는가?

< Default 디스패처가 CPU 집약적 작업에 적합한 이유 >

공식문서엔 Default 디스패처에 대한 구체적 지침이 없다. 따라서 공식적으로 쓸 이유가 전혀 없다. 사실만 보면 그렇다
그럼 CPU를 쓰는 작업에 Default 디스패처를 써야 한다는 가정은 어디서 나온 것인가? 결국 많은 문서에서 권장하는 게 그것이다. 이렇게 많은 작성자가 실수로 공식문서를 정확히 같은 방식으로 잘못 해석했을 가능성이 있는가? 아니다
Default 디스패처에 대한 가이드라인은 단순히 RxJava의 Schedulers.computation()에서 이월(carried-over)된 것이다. 해당 문서엔 쓸 때와 쓰지 말아야 할 때가 명시적으로 설명돼 있다

- 이벤트 루프, 콜백 처리, 기타 연산 작업에 쓸 수 있다
- 이 스케줄러에서 블로킹, IO, 바인딩 작업을 수행하는 건 권장되지 않는다. 대신 io()를 써라

이 내용은 인터넷에서 찾을 수 있는 Default 디스패처에 대한 정확한 설명이다. 이제 Default 디스패처에 대해 널리 알려진 가정이 실제로 RxJava라고 치면 이런 이월이 왜 발생했는지 이해하는 게 중요하다
이유는 간단하다. RxJava의 Schedulers.computation()과 코루틴의 Default 디스패처는 모두 NUM_OF_CPU_CORES 쓰레드에 의해 지원된다(backed). 따라서 CPU 코어 수만큼의 쓰레드를 사용하면 쓰레드 풀을 CPU 집약적 작업에 적합하게 만들 수 있다. 왜 이 크기의 쓰레드 풀이 CPU 집약적 작업에 최적인가?...(중략)

 

공식문서와 다른 관점으로 쓰여진 글이지만 이 글의 주장을 전부 그대로 적용하기보다 프로젝트 특성에 따라 디스패처를 선택하는 게 중요하다. 이 글이 5년 전에 작성된 것인 만큼 더더욱 그렇다.

반응형
Comments