[Android] Kotlin Coroutine 기본 개념 익히기
Android

[Android] Kotlin Coroutine 기본 개념 익히기

728x90

 

 

Coroutine이란?

 안드로이드에서 비동기 프로그래밍을 하기 위해선 AsyncTask나 Rx 등 여러 솔루션들이 있습니다. 코루틴 역시 비동기 프로그래밍을 하기 위해 나온 개념입니다. 코루틴을 이용하면 메모리를 효율적으로 사용하면서 손쉽게 비동기 처리를 할 수 있습니다.

 코루틴은 코틀린 언어의 하위 개념이 아닌, 오래전부터 C#, Python, Go 등의 다양한 언어에서 지원하고 있던 개념입니다. 코루틴은 Co + Routines의 약자로써 Co는 Cooperation, Routines은 functions을 의미합니다. 즉, 서로 협력하는 함수들이라는 의미로, 여러 함수들이 번갈아가면서 실행되어 비동기적인 프로그래밍이 가능합니다.

https://www.youtube.com/watch?v=F63mhZk-1-Y

 코루틴은 흔히 경량 스레드라고 불립니다. 코루틴은 스레드 위에서 실행되는 하나의 일(Job)이라고 생각하시면 됩니다.

 

 

 

 

Coroutine 시작하기

 코루틴을 사용하기 위해 app 수준의 build.gradle에 dependency를 추가해줍니다.

dependencies {
  implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0'
  implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0'
}

 

 

 

 

Coroutine 맛보기

 코루틴을 맛보기 위해 간단한 예제를 가져왔습니다.

println("start in main thread")
GlobalScope.launch(Dispatchers.Default) {
    delay(300)
    println("do something in coroutine")
}
println("end in main thread")

// 결과
start in main thread
end in main thread
do something in coroutine

 launch { ... }는 코루틴에서 작업을 수행하는 명령어입니다. 괄호 안의 작업들은 비동기적으로 수행됩니다. 코드를 보면 delay() 함수가 실행되어 세 번째 로그가 나중에 찍힐 것 같았지만, 코루틴은 비동기적으로 동작하기 때문에 메인 스레드의 코드들이 먼저 호출되었습니다.

 

 코루틴은 기본적으로 CoroutineContext, CoroutineScope, Builder 세 가지 요소로 구성되어 있습니다.

  • CoroutineContext : 코루틴이 실행될 Context로, 코루틴의 실행 목적에 맞게 실행될 특정 스레드 풀을 지정해줍니다.
  • CoroutineScope : 코루틴을 제어할 수 있는 범위를 뜻합니다. 여기서의 제어는 어떤 작업을 취소하거나, 끝날 때까지 기다리는 것을 뜻합니다.
  • Builder : 코루틴을 실행하는 함수입니다. 종류로는 launch, async 등이 있습니다.

 자세한 설명은 아래에서 이어서 하도록 하겠습니다.

 

 

 

CoroutineContext

 코루틴은 여러 함수를 번갈아가면서 동작할 수 있으며, 코루틴이 실행되는 스레드를 지정할 수 있습니다. 스레드를 지정해줘야 하는 이유는, 실행하는 동작에 따라 빠르게 처리되어야 하는 것이 있고, I/O 작업처럼 오래 걸리는 동작들도 있습니다. 예를 들어, 안드로이드의 UI 작업은 메인 스레드에서만 수행되어야 하기 때문에 메인 스레드에서 코루틴이 실행되어야 합니다.

 CoroutineContext의 종류로는 Dispatchers, Job... 등이 있습니다. Dispatchers의 종류는 총 4가지이며 다음과 같습니다. (Job에 대한 설명은 좀 더 아래에서 하도록 하겠습니다.)

  • Dispatchers.Main : 메인 스레드입니다. 안드로이드에서 UI 작업을 처리할 때는 항상 여기서 수행되어야 합니다.
  • Dispatchers.IO : I/O 작업하는데 최적화되어 있는 스레드입니다. 네트워크 처리나 AAC Room 같은 DB 처리 같은 작업을 할 때 주로 사용됩니다.
  • Dispatchers.Default : 그 외에 CPU 사용량이 많은 작업을 할 때 사용합니다. 크기가 큰 리스트를 다루거나 필터링을 하는 등의 무거운 연산이 필요한 작업들을 처리할 때 사용합니다.
  • Dispatchers.Unconfined : 다른 Dispatchers와 달리 특정 스레드를 지정하지 않습니다. 일반적으로는 사용하지 않습니다.

 

GlobalScope.launch(Dispatchers.Main) {
    launch(Dispatchers.IO) {
        println("do something in IO thread")
    }
    
    launch(Dispatchers.Default) { 
        println("do something in Default thread")
    }
}

 위의 예시에서는 메인 스레드에서 코루틴을 실행한 뒤, 몇몇 코루틴은 다른 스레드에서 실행하도록 처리하고 있습니다. 이처럼 코루틴을 사용하면 여러 함수를 번갈아가면서 동기스러운 코드로 비동기 프로그래밍을 할 수 있습니다.

 

 

 

 

CoroutineScope

CoroutineScope는 코루틴이 실행되는 범위를 뜻합니다. 대표적인 예시로는 GlobalScope가 있습니다. GlobalScope는 Application 범위에서 동작하는 Scope로써, Application이 살아있는 동안에는 코루틴을 제어할 수 있습니다.

만약 Activity에서 코루틴을 GlobalScope에서 실행시킨다면, Activity가 종료되더라도 코루틴은 작업이 끝날 때까지 동작하게 됩니다. 따라서 리소스 낭비가 발생할 수 있기 때문에, 코루틴을 사용할 때는 Scope를 알맞게 설정해줘야 합니다.

 

androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0
androidx.lifecycle:lifecycle-runtime-ktx:2.2.0

 AndroidX의 lifecycle 의존성을 추가해준다면, Activity나 ViewModel에 맞는 CoroutineScope를 사용할 수 있습니다.

class MainActivity {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        lifecycleScope.launch(Dispatchers.Default) {
            println("do something")
        }
    }
}

 

 

 

 

Builder

 Builder는 설정해준 Context와 Scope를 통해 코루틴을 실행할 수 있게 해 줍니다. 종류로는 launch, async 등이 있습니다.

val job = GlobalScope.launch {
    println("do something")
}

val deferred = GlobalScope.async {
    "result"
}

job.join()

val result = deferred.await()

 launch와 async는 동일한 기능을 하지만 다른 객체를 반환합니다. launch는 Job을 반환하고, async는 Deferred<T> 객체를 반환합니다. 이렇게 반환된 Job, Deferred 객체를 통해 각 코루틴을 제어할 수 있습니다.

 Job 객체는 join() 확장 함수를 사용하여 코루틴이 완료될 때까지 기다릴 수 있으며, Deferred 객체는 await() 함수를 통해 코루틴이 끝날 때까지 결과를 기다릴 수 있습니다.

 

val job = launch(start = CoroutineStart.LAZY) {
    // do something
}

val deferred = async(start = CoroutineStart.LAZY) {
    // do something
}

job.join() // or job.start()
deferred.await() // or deferred.start()

 CoroutineStart.LAZY를 사용하여 지연 실행도 가능해집니다. Job이나 Deferred를 미리 만들어 두고 특정 시점에 코루틴을 실행하게 할 수 있습니다.

 

GlobalScope.launch {
    val name = withContext(Dispatchers.IO) {
        "leveloper"
    }
    println("name: $name")    
}

 async와 유사한 withContext라는 함수도 있습니다. async 함수로 코루틴을 실행한 뒤 await()를 바로 실행한 것과 동일한 동작을 합니다. withContext는 코루틴 내에서만 동작 가능한 suspend function입니다.

 

 

 

 

suspend function

 suspend 함수는 다른 suspend 함수 혹은 코루틴 안에서만 실행이 가능합니다. 코루틴 외부에서 suspend 함수를 호출하려고 하면 아래와 같은 에러 메시지가 나오게 됩니다.

Suspend function (FUNCTION_NAME) should be called only from a coroutine or another suspend function

 따라서 코루틴 안에서 실행시키려는 함수는 suspend 키워드를 붙여줘야 합니다.

 

 다음은 suspend 함수의 예제입니다.

GlobalScope.launch {
    doSomething()
}

private suspend fun doSomething() {
    println("do something")
    
    CoroutineScope(Dispatchers.IO).launch {
        // do something
    }
}

 

 

 

 

Job

val job = GlobalScope.launch {
    println("do something")
}

 앞서 설명했듯이 위와 같은 방식으로 각 코루틴에 대한 Job을 반환받아 각각의 코루틴을 제어할 수 있었습니다. 하지만 한 CoroutineScope 내에 여러 개의 코루틴이 존재하고 그 코루틴들을 한 번에 관리하려면 어떻게 해야 할까요?

 Job 또한 CoroutineContext의 일종이기 때문에, 이를 이용하면 간단하게 관리가 가능해집니다.

fun main() = runBlocking {
    val job = Job()
    CoroutineScope(Dispatchers.Default + job).launch {
        launch {
            println("coroutine1 start")
            delay(1000)
            println("coroutine1 end")
        }
        launch {
            println("coroutine2 start")
            delay(1000)
            println("coroutine2 end")
        }
    }
    delay(300)
    job.cancel()
    delay(1000)
    println("all done !!!")
}

 하나의 Job 객체를 선언한 뒤, 새로 생성되는 CoroutineScope에서 객체를 초기화하면 이 CoroutineScope의 자식들까지 모두 영향을 받는 Job으로 활용이 가능합니다.

 

 

 

 

runBlocking

 위에서 Job을 설명할 때 runBlocking을 사용했습니다. 코루틴 공식 문서를 보면 종종 나오는 녀석인데요. 일반적으로 runBlocking은 사용이 권장되지 않습니다.

 runBlocking은 코루틴 실행 코드가 모두 완료될 때까지 현재 스레드를 blocking 하는 특징을 가지고 있습니다. 만약 메인 스레드에서 runBlocking을 사용하여 스레드를 장시간 점유하고 있을 경우 ANR (Application Not Responding)이 발생할 수 있습니다.

 

 ViewModel에서 runBlocking을 사용한 예제를 들어보겠습니다.

class MainViewModel : ViewModel() {
    fun loadData() = runBlocking {
        delay(10000)
    }
}

 만약 메인 스레드에서 loadData() 함수를 호출하게 되면 runBlocking을 통해 코루틴이 실행되게 되고, 10초 동안 메인 스레드를 Block 시키게 됩니다. 안드로이드에서는 UI 스레드가 5초 이상 응답이 없다면 ANR이 발생하여 앱이 죽을 수 있습니다.

따라서 runBlocking은 되도록이면 사용을 자제하고 필요하다면 용도를 명확하게 하여 조심스럽게 사용하는 것을 추천합니다.

 

 

 

 

참고

https://codechacha.com/ko/android-coroutine/

 

안드로이드 - Kotlin Coroutine을 사용하는 방법

Kotlin Coroutine은 가벼운 쓰레드(Light-weight thread)입니다. 비동기적인(asynchronous) 프로그래밍이 가능하게 만들어 줍니다. async, launch, coroutine context 등의 코루틴 키워드를 설명하고 어떻게 코루틴을 작

codechacha.com

https://kotlinlang.org/docs/coroutines-guide.html

 

Coroutines guide | Kotlin

 

kotlinlang.org

https://developer.android.com/topic/libraries/architecture/coroutines?hl=ko 

 

아키텍처 구성요소로 Kotlin 코루틴 사용  |  Android 개발자  |  Android Developers

Kotlin 코루틴은 비동기 코드를 작성할 수 있게 하는 API를 제공합니다. Kotlin 코루틴을 사용하면 코루틴이 실행되어야 하는 시기를 관리하는 데 도움이 되는 CoroutineScope를 정의할 수 있습니다. 각

developer.android.com

 

728x90