본문 바로가기
개인공부/android

[Kotlin] Coroutines (코루틴)

by 왕큰새 2022. 2. 1.
728x90

코루틴이란?

비동기적으로 실행되는 코드를 간소화하기 위해 Android에서 사용할 수 있는 동시 실행 설계 패턴이다.

 

코루틴을 사용할 때의 이점

  • 기본 스레드를 차단하여 앱이 응답하지 않게 만들 수도 있는 장기 실행 작업을 관리하는 데 수월하다.
  • 코루틴을 사용하면 세부적인 제어를 통해 스레드를 전달할 수 있다.
  •  withContext()를 사용하면 콜백을 도입하지 않고도 코드 줄의 스레드 풀을 제어할 수 있으므로 데이터베이스에서 읽기, 네트워크 요청 실행과 같은 매우 작은 함수에 이를 적용할 수 있다. 

코루틴 기능 4가지

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

라고 Android 공식 문서에 나와있다.

 

장기 실행 작업 관리

코루틴은 장기 실행 작업을 처리하는 두 작업을 추가하여 일반 함수를 기반으로 빌드된다. ( suspend 및 resume 추가 )

 

  • suspend는 현재 코루틴 실행을 일시중지하고 모든 로컬 변수를 저장합니다.
  • resume은 정지된 위치부터 정지된 코루틴을 계속 실행합니다.

suspend 함수는 다른 suspend 함수에서 호출하거나, 코루틴 빌더(예: launch)를 사용하여

새 코루틴을 시작하는 방법으로만 호출할 수 있다.

 

장기 실행 작업의 간단한 코루틴 구현 예시

suspend fun fetchDocs() {                             // Dispatchers.Main
    val result = get("https://developer.android.com") // Dispatchers.IO for `get`
    show(result)                                      // Dispatchers.Main
}

suspend fun get(url: String) = withContext(Dispatchers.IO) { /* ... */ }

함수 get()은 기본 스레드에서 실행 되지만 네트워크 요청을 시작하기 전에 코루틴을 정지한다.

네트워크 요청이 완료되면 get은 콜백을 사용해 정지된 코루틴을 재개한다.

 

 

안전을 위한 코루틴 사용

Kotlin 코루틴은 디스패처를 사용하여 코루틴 실행에 사용되는 스레드를 확인한다.

코드를 기본 스레드 외부에서 실행하려면 기본 또는 IO 디스패처에서 실행하도록 한다.

Kotlin에서 모든 코루틴은 기본 스레드에서 실행 중인 경우에도 디스패처에서 실행되어야 한다. 

코루틴은 자체적으로 정지될 수 있고, 디스패처는 코루틴 재개를 담당한다.

 

Kotlin의 세가지 디스패처(Dispatchers)

  • Dispatchers.Main - 이 디스패처를 사용하여 기본 Android 스레드에서 코루틴을 실행합니다. 이 디스패처는 UI와 상호작용하고 빠른 작업을 실행하기 위해서만 사용해야 합니다. 예를 들어 suspend 함수를 호출하고 Android UI 프레임워크 작업을 실행하며 LiveData 객체를 업데이트합니다.
  • Dispatchers.IO - 이 디스패처는 기본 스레드 외부에서 디스크 또는 네트워크 I/O를 실행하도록 최적화되어 있습니다. 예를 들어 Room 사용하고 파일에서 읽거나 파일에 쓰며 네트워크 작업을 실행합니다.
  • Dispatchers.Default - 이 디스패처는 CPU를 많이 사용하는 작업을 기본 스레드 외부에서 실행하도록 최적화되어 있습니다. 예를 들어 목록을 정렬하고 JSON을 파싱합니다.

라고 Android 공식 문서에 나와있다.

 

디스패처를 사용한 코루틴 구현 예시

suspend fun fetchDocs() {                      // Dispatchers.Main
    val result = get("developer.android.com")  // Dispatchers.Main
    show(result)                               // Dispatchers.Main
}

suspend fun get(url: String) =                 // Dispatchers.Main
    withContext(Dispatchers.IO) {              // Dispatchers.IO (main-safety block)
        /* perform network IO here */          // Dispatchers.IO (main-safety block)
    }                                          // Dispatchers.Main
}

함수 get() 에서 withContext(Dispatchers.IO)를 호출하여, IO 스레드 풀에서 실행되는 블록을 만든다.

블록안의 코드는 항상 IO 디스패처를 통해 실행된다.

withContext는 그 자체로 정지 함수이므로 get 함수도 정지 함수이다.

 

withContext()를 사용하여 모든 함수가 기본 스레드에서 함수를 호출할 수 있는지 확인하는 것이 좋다.

호출자는 함수를 실행하는 데 사용해야 할 스레드를 생각할 필요가 없다.

 

중요: suspend를 사용해도 백그라운드 스레드에서 함수를 실행하도록 Kotlin에 지시하지 않는다.

일반적으로 suspend 함수는 기본 스레드에서 작동한다. 기본 스레드에서 코루틴을 실행하는 것이 일반적이다.

디스크에서 읽기 또는 디스크에 쓰기, 네트워크 작업 실행, CPU 집약적인 작업 실행 등과 같은

기본적인 안전이 요구되는 withContext()는 항상 suspend 함수 내에서 사용해야 한다.

 

 

코루틴 시작

코루틴 시작에는 두가지 방법이 있다.

 

  • launch는 새 코루틴을 시작하고 호출자에게 결과를 반환하지 않습니다. '실행 후 삭제'로 간주되는 모든 작업은 launch를 사용하여 시작할 수 있습니다.
  • async는 새 코루틴을 시작하고 await라는 정지 함수로 결과를 반환하도록 허용합니다.

라고 Android 공식 문서에 나와있다.

 

 

코루틴 개념

CoroutineScope

  • CoroutineScope는 launch 또는 async를 사용하여 만든 코루틴을 추적한다. 
  • 실행중인 코루틴은 scope.cancel()을 사용하여 취소할 수 있다.
  • 일부 KTX 라이브러리는 특정 수명 주기 클래스에 자체 CoroutineScope를 제공한다.

ViewModel -> viewModelScope

Lifecycle -> lifecycleScope 

 

 

CoroutineScope 만드는 예제

class ExampleClass {

    // Job and Dispatcher are combined into a CoroutineContext which
    // will be discussed shortly
    val scope = CoroutineScope(Job() + Dispatchers.Main)

    fun exampleMethod() {
        // Starts a new coroutine within the scope
        scope.launch {
            // New coroutine that can call suspend functions
            fetchDocs()
        }
    }

    fun cleanUp() {
        // Cancel the scope to cancel ongoing coroutines work
        scope.cancel()
    }
}

 

취소된 범위에서는 코루틴을 만들 수 없다. 위의 예제 같은 경우 ExampleClass의 범위

따라서, 수명 주기를 제어하는 클래스가 제거되는 경우에만 scope.cancel()을 호출해야 한다.

 

viewModelScope를 사용하는 경우 viewModel 클래스는 onCleared() 메서드에서 자동으로 범위를 취소한다.

 

viewModelScope 사용 예제

class MyViewModel: ViewModel() {
    init {
        viewModelScope.launch {
            // ViewModel이 지워지면 취소될 코루틴
        }
    }
}

 

작업 (Job)

lanuch 또는 async로 만드는 코루틴은 고유하게 식별하고 수명주기를 관리하는 Job 인스턴스를 반환한다.

다음 예와 같이 Job을 CoroutineScope에 전달하여 코루틴의 수명주기를 관리할 수 있다.

class ExampleClass {
    ...
    fun exampleMethod() {
        // 코루틴의 수명 주기를 제어할 핸들, Job 인스턴스
        val job = scope.launch {
            // New coroutine
        }

        if (...) {
            // Job 인스턴스가 관리하는 Coroutine을 취소한다.
            job.cancel()
        }
    }
}

 

 

CoroutineContext

CoroutineContext는 일련의 다음 요소를 사용하여 코루틴의 동작을 정의한다.

라고 Android 공식 문서에 나와있다.

 

 

범위 내에 만들어진 새 코루틴의 경우 새 Job 인스턴스가 새 코루틴에 할당되고 다른 CoroutineContext 요소는 포함 범위에서 상속됩니다. 새 CoroutineContext를 launch 또는 async 함수에 전달하여 상속된 요소를 재정의할 수 있습니다. Job을 launch 또는 async에 전달해도 아무런 효과가 없습니다. Job의 새 인스턴스가 항상 새 코루틴에 할당되기 때문입니다.

 

범위 내에( 아래 예제의 경우 ExampleClass ) 만들어진 코루틴의 경우 새로운 Job 인스턴스가

만들어진 코루틴에 할당된다. 다른 CoroutineContext 요소는 포함 범위에서 상속된다. 

새 CoroutineContext를 launch 또는 async 함수에 전달하여 상속된 요소를 재정의 할 수 있다.

 

아래 예제의 경우,

  • job1의 Coroutine 이름이 coroutine이고, Dispatchers 는 Main이다.
  • job2의 Coroutine 이름이 BackgroundCoroutine이고, Dispatchers 는 Defalut로 재정의된다.
class ExampleClass {
    val scope = CoroutineScope(Job() + Dispatchers.Main)

    fun exampleMethod() {
        // Starts a new coroutine on Dispatchers.Main as it's the scope's default
        val job1 = scope.launch {
            // New coroutine with CoroutineName = "coroutine" (default)
        }

        // Starts a new coroutine on Dispatchers.Default
        val job2 = scope.launch(Dispatchers.Default + "BackgroundCoroutine") {
            // New coroutine with CoroutineName = "BackgroundCoroutine" (overridden)
        }
    }
}