안드로이드 Coroutine 란? (1)

쓰리원·2022년 6월 6일
0

Coroutine 정리

목록 보기
2/4
post-thumbnail

코루틴이 무엇인지 개요 정도로 간단한 개념을 이해할 수 있는 글 입니다.
원본 글 : https://leveloper.tistory.com/209 [꾸준하게:티스토리]
편하게 복습하면서 보려고 옮겨놓습니다.

미리 알아두면 좋은 선수지식 : Coroutine과 Thread 비교

1. Coroutine이란?

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

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

코루틴은 스레드 위에서 실행되는 하나의 일(Job) 입니다.

fun main() = runBlocking {
	println("start in main thread")
    launch {
    	delay(1000L)
        println("launch: ${Thread.currentThread().name}")
        println("do something in coroutine")
    }
    println("runBlocking: ${Thread.currentThread().name}")
    println("end in main thread")
}

결과

start in main thread
runBlocking: main @coroutine#1
end in main thread
launch: main @coroutine#2
do something in coroutine

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

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

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

2. CoroutineContext

 코루틴은 여러 함수를 번갈아가면서 동작할 수 있으며, 코루틴이 실행되는 스레드를 지정할 수 있습니다. 사용하는 용도에 따라 I/O 작업의 경우는 IO, UI 작업의 경우 Main과 같이 스레드를 지정해서 코루틴을 실행하게 됩니다.

 CoroutineContext의 종류로는 Dispatchers, Job... 등이 있습니다. Dispatchers의 종류는 총 4가지이며 다음과 같습니다.

  • Dispatchers.Main

메인 스레드입니다. 안드로이드에서 UI 작업을 처리합니다

  • Dispatchers.IO

I/O 작업을 하는 스레드입니다. 네트워크나 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")
    }
}

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

3. 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")
        }
    }
}

4. 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 객체를 반환합니다. 이렇게 반환된 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입니다.

5. 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
    }
}

6. 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 !!!")
}

출력 결과

coroutine1 start
coroutine2 start
all done !!!

 coroutine1 end나 coroutine2 end가 출력되지 않고, job.cancel() 함수에 의해 모든 작업이 취소됩니다. 이렇듯, 하나의 Job 객체를 선언한 뒤, 새로 생성되는 CoroutineScope에서 객체를 초기화하면 이 CoroutineScope의 자식들까지 모두 영향을 받는 Job으로 활용이 가능합니다.

7. 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이 발생하여 앱이 죽을 수 있습니다.

8. reference

https://codechacha.com/ko/android-coroutine/
https://kotlinlang.org/docs/coroutines-guide.html https://developer.android.com/topic/libraries/architecture/coroutines?hl=ko
https://leveloper.tistory.com/209 [꾸준하게:티스토리]
https://www.charlezz.com/?p=45962

profile
가장 아름다운 정답은 서로의 협업안에 있다.

0개의 댓글