봄날은 갔다. 이제 그 정신으로 공부하자

Coroutine - part1 본문

android Tip

Coroutine - part1

길재의 그 정신으로 공부하자 2020. 12. 25. 10:13

코루틴은 비동기적으로 실행되는 코드를 간소화히기 위해 Android에서 사용할 수 있는 동시 실행 설계 패턴으로 Kotlin Ver 1.3에 추가 되었습니다. 코루틴은 Python, C#, Go, JavaScript 등 여러 언어에서 지원하고 있는 개념입니다.

이 글은 몇가지 예제로 코루틴 사용 방법에 대해 설명합니다.

 

참고 사이트: 

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

 

 

Thread and Coroutine

코루틴은 Android의 비동기 프로그래밍에 권장되는 솔루션으로 비동기 처리를 위해 이전에 사용하던 구린 AsyncTask를 대체할 효율적인 라이브러리 입니다.

Android는 ANR을 방지하기 위해 앱이 Main Thread에서 Network, DB, File 등에 접근하는 것을 제한하고 있습니다.

이러한 것들을 처리하기 위해서는 Main Thread가 아닌 Background Thread에서 처리해야 하는 것이지요.

비동기 처리를 위해 Rx를 사용하거나 앞서 언급한 AsyncTask를 사용할 수 있지만 Rx는 진입장벽이 조금 높고 AsyncTask는 Deprecated 되어 계속 사용하기에는 부담스러운 것이 현실 입니다.

 

코루틴은 Co(협력, 같이)와 Routine(일련의 명령)이라는 두 단어의 합성어입니다.

하나의 Thread가 끝날 때까지 계속 되는 것과 달리 코루틴은 실행 중간에 다른 작업을 하러 갔다가 다시 돌아와서 작업을 다시 할 수 있습니다. 코루틴은 Thread와 기능적으로 같지만, Thread에 비교하면 좀더 가볍고 유연하며 한단계 더 진화된 병렬 프로그래밍을 위한 기술입니다.  하나의 Thread 내에서 여러개의 코루틴이 실행되는 개념으로 동일한 기능을 Thread와 코루틴으로 구현하면 아래와 같습니다.

// Thread
Thread(Runnable {
        Thread.sleep(1000L)
        Log.d(TAG, "Working in Thread.”)
    }
}).start()
// Coroutine
Log.d(TAG, "Main Thread - Start")
GlobalScope.launch {
        delay(1000L)
        Log.d(TAG, "Working in Coroutine.")
    }
Log.d(TAG, "Main Thread - End")

코루틴은 GlobalScope.launch 정의되며 { … }으로 묶인 코드가 비동기적으로 실행됩니다.

위 예제의 실행 결과 로그는 아래와 같이 출력 됩니다.

>> Main Thread - Start

>> Main Thread - End

>> Working in Coroutine.

 

코루틴 적용하기

app 레벨의 build.gradle에 아래와 같이 디펜던시를 추가해줍니다.

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.5’
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.5’

 

코루틴 알아보기

Kotlin 코루틴은 Dispatcher를 사용하여 코루틴 실행에 사용되는 Thread를 확인합니다. 

코드를 기본 Thread 외부에서 실행하려면 기본 또는 IO Dispatcher에서 작업을 실행하도록 Kotlin 코루틴에 지시하면 됩니다. 

Kotlin에서 모든 코루틴은 기본 Thread에서 실행 중인 경우에도 Dispatcher에서 실행되어야 합니다. 

코루틴은 자체적으로 정지될 수 있으며 Dispatcher는 코루틴 재개를 담당합니다.

Kotlin은 코루틴을 실행할 위치를 지정하는 데 사용할 수 있는 세 가지 Dispatcher를 제공합니다.

 

Dispatchers.Main

Dispatcher를 사용하여 기본 Android Thread에서 코루틴을 실행합니다. 

이 디스패처는 UI와 상호작용하고 빠른 작업을 실행하기 위해서만 사용해야 합니다. 

예를 들어 suspend 함수를 호출하고 Android UI 프레임워크 작업을 실행하며 LiveData 객체를 업데이트합니다.

 

Dispatchers.IO

Dispatcher는 기본 Thread 외부에서 디스크 또는 네트워크 I/O를 실행하도록 최적화되어 있습니다. 

 

Dispatchers.Default

Dispatcher는 CPU를 많이 사용하는 작업을 기본 스레드 외부에서 실행하도록 최적화되어 있습니다. 

예를 들어 목록을 정렬하고 JSON을 파싱합니다.

 

runBlocking 블록 사용하기

위 언급한 예제를 다시 언급하면 아래 예제는 로그가 아래와 같이 출력 됩니다.

>> Main Thread - Start

>> Main Thread - End

>> Working in Coroutine.

Log.d(TAG, "Main Thread - Start")
GlobalScope.launch {
        delay(1000L)
        Log.d(TAG, "Working in Coroutine.")
    }
Log.d(TAG, "Main Thread - End")

 

만약 순서를 보장하고자 한다면 아래와 같이 runBlocking{ … }을 사용해주면 됩니다.

Main Thread는 runBlocking{ … } 내부 처리가 완료 될때까지 suspend 상태가 됩니다.

Log.d(TAG, "Main Thread - Start")
runBlocking {
    delay(1000L)
    Log.d(TAG, "Working in Coroutine.")
}
Log.d(TAG, "Main Thread - End")

 

// 실행 결과

>> Main Thread - Start

>> Working in Coroutine.

>> Main Thread - End

 

runBlocking{ … } 스코프 내에서도 launch{ … }와 coroutineScope{ …}를 사용할 수 있습니다.

아래 예제의 실행 결과는 아래와 같습니다.

// 실행 결과

>> Main Thread - Start

>> end coroutineScope

>> launch #2

>> end block

>> launch #1

>> Main Thread - End

Log.d(TAG, "Main Thread - Start")
runBlocking {
    launch {
        delay(2000L)
        Log.d(TAG, "launch #1")
    }

    coroutineScope {
        launch {
            delay(1000L)
            Log.d(TAG, "launch #2")
        }
        Log.d(TAG, "end coroutineScope")
    }

    Log.d(TAG, "end block")
}
Log.d(TAG, "Main Thread - End")

 

Job을 사용해 동기화 처리하기

위 예제는 sleep()을 사용하고 있는데 위 실행 순서를 아래와 같이 출력되게 하기 위해 sleep()을 사용하는 것은 매우 매우 비효율적입니다.

launch에서 반환하는 Job을 사용하여 비동기 처리를 부분적으로 동기 처리할 수 있습니다.

>> Main Thread - Start

>> launch #1

>> launch #2

>> end coroutineScope

>> end block

>> Main Thread - End

 

위와 같이 출력되게 하기 위해 job을 사용하면 아래와 같습니다.

아래와 같이 작업하면 비동기이지만 필요한 경우 순서 보장이 되므로 순서 보장을 위해 불필요한 sleep()을 사용할 필요가 없습니다.

Log.d(TAG, "Main Thread - Start")
runBlocking {
    val job1 = launch {
        Log.d(TAG, "launch #1")
    }
    job1.join()

    coroutineScope {
        val job2 = launch {
            Log.d(TAG, "launch #2")
        }
        job2.join()
        Log.d(TAG, "end coroutineScope")
    }
    Log.d(TAG, "end block")
}
Log.d(TAG, "Main Thread - End")

 

Job은 join() 함수외에도 코루틴 작업 블록(launch{…})을 종료 해주는 cancel()함수와 cancelAndJoin()를 지원합니다.

아래 코드는 코루틴 작업 블록이 100번 반복되지만 실제적으로는 500ms의 delay후에 job.cancelAndJoin()함수를 만나 해당 작업 블록이 종료 됩니다.

Log.d(TAG, "Main Thread - Start")
runBlocking {
    val job = launch {
        repeat(100) {
            delay(200L)
            Log.d(TAG, "launch #${it}")
        }
    }
    delay(500L)
    job.cancelAndJoin()
    Log.d(TAG, "end Block")
}
Log.d(TAG, "Main Thread - End")
Comments