1. 주요 개념 정리

1.1. CoroutineDispatcher 란?

CoroutineDispatcher 란?

코루틴을 스레드로 보내주는 역할을 하는 객체

1.2. CoroutineDispatcher 의 동작 방식

1.2.1 CoroutineDispatcher 의 동작 다이어그램

sequenceDiagram
    actor 사용자
    participant CoroutineDispatcher
    participant 스레드풀

    사용자 ->> CoroutineDispatcher: 신규 작업(코루틴 A) 처리 요청
    CoroutineDispatcher ->> CoroutineDispatcher: `코루틴 A`를 작업 대기열에 적재
    loop 스레드가 사용 가능해질 때까지 대기
        CoroutineDispatcher ->> 스레드풀: 사용 가능한 스레드 확인
        alt 사용 가능한 스레드가 있음
            스레드풀 -->> CoroutineDispatcher: 사용 가능한 스레드 있음
            CoroutineDispatcher ->> 스레드풀: `코루틴 A` 적재
        else 모든 스레드가 바쁨
            스레드풀 -->> CoroutineDispatcher: 모든 스레드가 바쁨
        end
    end
  1. 사용자가 CoroutineDispatcher에게 코루틴 A 라는 작업을 요청한다.
  2. CoroutineDispatcher는 작업 대기열에 코루틴 A 를 적재한다.
  3. CoroutineDispatcher스레드 풀에 사용 가능한 스레드가 있는지 확인한다.
    1. 사용 가능한 스레드가 있다면,
      1. 해당 스레드에 코루틴 A를 적재한다.
    2. 모든 스레드에 작업이 할당되어 있다면,
      1. 사용 가능한 스레드가 생길 때까지 대기한다.

1.3. CoroutineDispatcher 의 종류

디스패처는 제공 가능한 스레드 개수 제한 여부를 기준으로 크게 두 가지로 나눈다. 스레드 생성 비용은 매우 비싸기 때문에, 가능한 제한된 디스패처를 사용하는 것이 좋다. 무제한 디스패처를 사용할 경우, 주의를 기울이는 것이 필요하다.

종류생성 가능한 스레드 개수
제한된 디스패처(Confined Dispatcher)제한 있음
무제한 디스패처(Unconfined Dispatcher)제한 없음

1.4. CoroutineDispatcher 생성 방법

1.4.1. 단일 스레드 디스패처 만들기

함수 newSingleThreadContext 를 사용하여 만들 수 있다.

val dispatcher: CoroutineDispatcher = newSingleThreadContext(
	name = "SingleThread",
)

1.4.1. 멀티 스레드 디스패처 만들기

함수 newFixedThreadPoolContext 을 사용하여 만들 수 있다. 이 때, 단일 스레드 디스패처와는 다르게 최대 가용 스레드의 개수를 지정해줄 수 있다.

val multiThreadDispatcher: CoroutineDispatcher = newFixedThreadPoolContext(
	nThreads = 2, // 최대 가용 스레드의 개수
	name = "MultiThread",
)

1.5. 이미 선언된 CoroutineDispatcher 종류

사실, 4. CoroutineDispatcher 생성 방법에 등장한 방법으로 CoroutineDispatcher를 직접 만들어 사용하는 것은 위험할 수 있다.

실제 코드로 작성해보면 IDE에서 다음과 같은 경고 창을 띄운다.

This is a delicate API and its use requires care. Make sure you fully read and understand documentation of the declaration that is marked as a delicate API.

newSingleThreadContext는 섬세한 주의가 필요한 API이니, 문서를 정독한 후 사용할 것을 권유한다. newFixedThreadPoolContext 도 마찬가지다. 개발자가 Dispatcher 를 직접 선언 할 경우, newFixedThreadPoolContext 함수를 사용해 CoroutineDispatcher 객체를 만들게 되면 특정 CoroutineDispatcher 객체에서만 사용되는 스레드풀이 생성되며, 스레드풀에 속한 스레드의 수가 너무 적거나 많이 생성돼 비효율적으로 동작할 수 있다. 또한 협업을 진행하는 경우, 다른 개발자가 선언한 디스패처의 존재를 몰라 새로운 Dispatcher를 생성하여 리소스가 낭비되는 오류를 범하기도 쉽다.

따라서, CoroutineDispatcher를 직접 선언하기 보다는 후술할 미리 선언된 Dispatcher를 사용하는 것이 좋다.

미리 선언된 Dispatcher 간단 요약

Dispatcher제한 여부용도특이사항limitedParallelism 사용 시, 발생하는 일
Dispatchers.IO제한된 디스패처IO bound 작업최대 64개 (1.7.2 버전 기준) 혹은 JVM 최대 범위만큼만 스레드를 생성할 수 있다.신규 스레드를 생성한다.
Dispatchers.Default제한된 디스패처CPU bound 작업코루틴 사용 여부와 관계없이 처리 속도는 일정하다.공유 스레드 풀에 있는 스레드를 사용한다.
Dispatchers.Main제한된 디스패처UI bound 작업UI 표시와 관련된 별도의 의존성 추가가 필요하다.-
Dispatchers.Unconfined무제한 디스패처---

1.5.1. Dispatchers.IO

Dispatchers.IO 는 제한된 디스패처로 서버와 통신을 하거나 파일 입출력 등 대기 시간이 긴 작업에 사용되는 디스패처입니다.

Dispatchers.IO 를 사용하여 애플리케이션 외부에 응답 요청하는 코루틴 작업을 수행하면, 응답이 도착하기 전까지 다른 작업들에게 자신이 사용하던 스레드 양도합니다. 이러한 특징 덕분에 입출력 작업 시, 다른 스레드를 막지 않아 애플리케이션 연산이 전반적으로 빨라집니다.

Dispatchers.IO 는 최대로 사용 가능한 스레드의 개수가 코틀린 버전 1.7.2 기준 64개, 혹은 JVM 최대 가용 갯수 중 큰 수만큼 스레드를 사용합니다.

공유 스레드 풀에서 사용할 스레드를 선택합니다.

1.5.2. Dispatchers.Default

Dispatchers.Default 는 무거운 연산을 처리할 때에 주로 사용하는 코루틴 디스패처입니다.

Dispatchers.Default는 연산 요청이 들어오면, Dispatchers.IO 와 공유하는 스레드 풀에서 사용할 스레드를 선택하여 작업을 처리합니다. 만약, 연산 요청이 크면 클수록, 디스패처는 스레드를 많이 점유하여 사용하려고 합니다.

공유 스레드 풀에 있는 스레드 자원을 아끼기 위해서는 limitedParallelism 으로, 최대 사용 가능한 스레드의 개수를 제한해야합니다.

fun main() = runBlocking<Unit > {
	launch(Dispatchers.Default.limitedParallelism(2)){
		repeat (10) {
			launch {
				printIn("[${Thread.rrentThread(n)}]")
			}
		}
	}
}

Dispatchers.IO에도 limitedPrallelism 을 사용할 수 있나요?

네. 그러나, Dispatchers.Default 와는 다르게 Dispatchers.IO에서 limitedPrallelism를 호출할 경우, 인자로 전달된 숫자만큼 신규 스레드를 공유 풀에 생성합니다. 스레드를 생성하는 작업은 비싸기 때문에, 충분한 고려 후 Dispatchers.IO에서 limitedPrallelism를 호출하는 것이 좋습니다.

1.5.3. Dispatchers.Main

Dispatchers.Main 는 화면에 요소를 표시하는 작업에 주로 사용됩니다.

따라서, Android 환경에서 많이 사용되며, 별도의 의존성(kotlin-coroutine-android) 을 추가해야 사용할 수 있습니다.

3. 질의 응답

Dispatchers.IO 는 최대 가용 스레드 수가 제한되어있는데, 왜 limitedParallelism을 사용할까요?

limitedParallelism(1) 과 newSingleThreadContext() 사용 가능한 최대 스레드 개수는 1개지만, limitedParallelism 는 실행 중간에 스레드가 바뀔 수 있다.

오래 걸리는 작업을 생성하는 데에는 Thread.sleep()이 좋다.

실전 적용. 이미지 변환 작업을 수행할 때, Dispatchers.Default를 사용하면 어떤 문제가 발생할 수 있을까요?

2. 코드 예제

예시: eunice-hong/read-kotlin-coroutine/Chapter03Activity

문제 상황 1: 네트워크 요청이 UI 성능에 미치는 영향

상황

어떤 모바일 애플리케이션에서 사용자가 버튼을 눌렀을 때 서버에서 데이터를 가져와 화면에 표시하는 기능을 구현해야 합니다. 처음에는 이 작업이 메인 스레드에서 동기적으로 이루어졌습니다. 그러나 사용자가 버튼을 눌렀을 때 네트워크 요청이 지연되면 UI가 멈추거나 “앱이 응답하지 않음(ANR)” 상태가 발생하는 문제가 생겼습니다. 이로 인해 사용자 경험이 크게 저하되었습니다.

해결책

이 문제를 해결하기 위해 Dispatchers.IO를 사용하여 네트워크 요청을 백그라운드 스레드에서 비동기적으로 처리하고, 응답이 완료되면 Dispatchers.Main을 사용하여 UI를 업데이트하는 방식으로 변경했습니다. 이로 인해 메인 스레드가 네트워크 요청에 의해 블로킹되지 않으면서도, UI는 사용자가 원활하게 조작할 수 있게 되었습니다.

실전 적용 결과

네트워크 요청이 백그라운드에서 처리되어 UI 응답성이 크게 향상되었습니다. 사용자는 버튼을 눌러도 UI가 멈추지 않고 자연스럽게 데이터가 로드되는 것을 확인할 수 있었습니다.

문제 상황: CPU 집약적인 데이터 처리로 인한 애플리케이션 느려짐

상황

한 데이터 분석 애플리케이션에서 사용자로부터 입력된 대량의 데이터를 처리해야 하는 기능이 있었습니다. 초기 구현에서는 이 데이터 처리 작업이 메인 스레드에서 동기적으로 수행되었습니다. 결과적으로, 데이터 처리 중 애플리케이션이 느려지고, 다른 작업을 수행할 수 없는 상황이 발생했습니다.

해결책

이 문제를 해결하기 위해 Dispatchers.Default를 사용하여 CPU 집약적인 데이터 처리를 백그라운드 스레드에서 수행하도록 코드를 리팩토링했습니다. 이 디스패처는 기본적으로 여러 코어를 활용하여 CPU 집약적인 작업을 병렬로 처리하기 때문에, 메인 스레드의 부하를 줄이면서도 효율적인 작업 처리가 가능해졌습니다.

실전 적용 결과

애플리케이션의 응답성이 크게 개선되었으며, 데이터 처리 중에도 다른 UI 작업이 원활하게 이루어졌습니다. 사용자는 데이터가 처리되는 동안에도 앱을 계속 사용할 수 있게 되었습니다.

문제 상황 3: 한정된 리소스 환경에서의 스레드 관리 문제

상황

한 서버 애플리케이션에서 동시에 많은 사용자의 요청을 처리해야 했습니다. 처음에는 각 요청마다 새로운 코루틴이 생성되었고, 이 코루틴들은 모두 Dispatchers.IO에서 실행되었습니다. 그러나 서버 자원이 한정되어 있어, 너무 많은 요청이 동시에 처리되면서 시스템이 과부하 상태에 빠졌습니다.

해결책

이 문제를 해결하기 위해 limitedParallelism을 사용하여, Dispatchers.IO에서 동시에 처리할 수 있는 스레드의 수를 제한했습니다. 이를 통해 한정된 자원을 효과적으로 관리할 수 있었고, 과부하 상태를 방지할 수 있었습니다. 동시에, 코루틴이 과도하게 생성되는 상황에서도 서버가 안정적으로 동작하도록 하였습니다.

실전 적용 결과

서버의 안정성이 크게 향상되었으며, 높은 트래픽 상황에서도 서버는 더 이상 과부하 상태에 빠지지 않았습니다. 사용자는 안정적인 서비스를 경험할 수 있었고, 서버 자원도 효율적으로 사용되었습니다. #11-coroutinedispa# 3. 실전 응용

해야할 일

  1. 실제 사용 사례: 실전에서 어떻게 사용될 수 있는지 사례를 소개합니다.
  2. 장단점 및 한계: 실전에서 사용할 때의 장점과 단점을 정리하고, 어떤 상황에서 주의가 필요한지 논의합니다.

장점

  1. 비동기 처리의 용이성 코루틴은 비동기 작업을 쉽게 처리할 수 있게 해줍니다. 스레드 풀을 관리할 필요 없이, launch와 async 같은 코루틴 빌더를 사용하여 비동기 작업을 간단하게 구현할 수 있습니다.
  2. 메모리 효율성 코루틴은 경량의 스레드처럼 동작하며, 실제로는 스레드보다 훨씬 적은 메모리를 사용합니다. 수천 개의 코루틴을 생성하더라도 시스템 자원을 효율적으로 사용할 수 있습니다.
  3. 코드 가독성 코루틴을 사용하면 비동기 코드도 동기 코드처럼 직관적으로 작성할 수 있습니다. 이는 콜백 지옥(callback hell)이나 복잡한 스레드 관리 코드를 피할 수 있게 합니다.

주의사항

  1. 스레드 관리 코루틴이 경량이기는 하지만, 실제로는 스레드 풀에서 실행됩니다. Dispatchers.Default와 같은 디스패처는 기본적으로 제한된 스레드를 사용하며, 너무 많은 코루틴이 동시에 실행되면 스레드 풀 고갈이나 성능 저하가 발생할 수 있습니다. 이럴 때는 limitedParallelism을 사용하여 동시 실행되는 코루틴 수를 제한하는 것이 중요합니다.
  2. 메모리 누수 코루틴이 잘못된 컨텍스트에서 실행되거나, 종료되지 않고 계속 유지되면 메모리 누수가 발생할 수 있습니다. 특히, 안드로이드에서는 액티비티나 프래그먼트의 생명주기에 따라 코루틴을 관리해야 합니다.
  3. 무제한 디스패처의 위험성 Dispatchers.Unconfined는 제한 없이 실행되기 때문에, 예기치 않은 동시성 문제나 리소스 고갈이 발생할 수 있습니다. 이 디스패처는 주로 가벼운 작업이나 특별한 상황에서만 사용해야 하며, 대부분의 경우 Dispatchers.IO나 Dispatchers.Default를 사용하는 것이 더 안전합니다.

4. 질의 응답

해야할 일

  1. 예상 질문 준비: 발표 내용을 기반으로 예상될 수 있는 질문을 미리 생각해보고 답변을 준비합니다. 질문을 받는 연습을 통해 발표 후 Q&A 세션에 대비합니다.
  2. 스터디 멤버와 토론: 발표 후에는 스터디 멤버들과 자유롭게 토론을 유도합니다. 멤버들의 이해도를 높이고 다양한 시각을 공유할 수 있습니다.

5. 참고 자료