Hun's Blog

[번역]Coroutines on Android (part I): Getting the background 본문

Android

[번역]Coroutines on Android (part I): Getting the background

jhk-im 2020. 11. 11. 16:28

medium.com/androiddevelopers/coroutines-on-android-part-i-getting-the-background-3e0e54d20bb

 

Coroutines on Android (part I): Getting the background

What problems do coroutines solve?

medium.com

코루틴은 어떤 문제를 해결할까?

코틀린 코루틴은 안드로이드에서 비동기 코드를 사용하는 새로운 Concurrency(동시발생) 스타일 소개한다. 

새로운 코틀린 1.3에서 처음 도입되었지만 코루틴의 개념은 프로그래밍 언어의 초기부터 존재했다. 

코루틴을 사용한 첫번째 언어는 1967년 Simula였다. 

 

최근 몇 년 동안 코루틴의 인기가 상승했고 현재 JavaScript, C#, Python, Ruby, Go 와 같은 인기있는 프로그래밍 언어에 포함되어있다. 코틀린 코루틴은 대형 applications를 구축하는 데 사용되어온 확립 된 컨셉에 기초한다.

 

안드로이드에서 코루틴은 두 가지 문제에 대한 훌륭한 solution이다. 

1. 장시간 실행되는 task는 메인 스레드를 너무 오랜시간 차단하는 task이다.

2. Main-safety를 통해 메인 스레드로부터 중단 스레드를 호출할수 있도록 확실히 보장한다.  

 

코루틴이 어떠한 방식으로 코드 structure에 도움을 주는지 알아보자.

 

Long running tasks

웹페이지를 가져오거나 API와 상호작용하는 것은 network request를 포함한다. 마찬가지로, 데이터베이스의 데이터에 접근하거나 이미지를 로드하는 것은 파일에 접근하는 것을 포함한다. 

이러한 종류의 작업들을 `Long runnig task`라고 볼 수 있다. -> App을 멈추고 기다리는데 오랜 시간이 걸리는 작업

 

우리가 사용하는 스마트폰 코드 실행이 network request보다 얼마나 빠르게 실행하는지 이해하기 어려울 수 있다. Pixel2에서 단일 CPU 사이클은 0.0000000004초 미만이며, 인간의 관점에서 파악하기 어려운 숫자이다.  그러나 네트워크 요청을 400ms(0.4초) 정도로 생각하면 CPU가 얼마나 빨리 동작하는지 이해하기 쉬울 것이다. 눈 깜짝할 사이보다 다소 느린 network request를 하는 동안 CPU는 10억 사이클 이상 실행될 수 있다. 

 

안드로이드에서 모든 앱이 UI 처리와 사용자 상호작용을 조정하는 메인 스레드를 가지고 있다. 메인 스레드에서 너무 많은 작업이 발생하면 앱이 멈추거나 느려지게 되어 좋지않은 사용자 경험으로 연결된다. Long running task는 메인 스레드를 차단하지 않아야 하므로, 애니메이션이 멈추거나 터치 이벤트에 느리게 반응하게 된다. 

 

메인 스레드에서 network request를 수행하기 위한 일반적인 패턴은 callback이다. callback으로 developer.android.com을 가져오는 것은 다음과 같이 보일 수 있다. 

class ViewModel: ViewModel() {
   fun fetchDocs() {
       get("developer.android.com") { result ->
           show(result)
       }
    }
}

메인스레드에서 get()을 호출하더라도 network request를 수행하기 위해 다른 스레드를 사용해야 한다. 그 다음 network result를 사용할 수 있게 되면, callback은 메인 스레드에서 호출된다. 이것은 Long running task를 처리하는 좋은 방법이며, retrofit과 같은 라이브러리에서 메인 스레드를 차단하지않고 netwrok request를 수행하도록 도와준다.

 

Using coroutines for long running tasks

코루틴은 fetchDocs처럼 Long running tasks 코드를 단순화하는 방법이다. 코루틴이 어떻게 Long running taks 코트를 단순하게 만드는지 알아보기 위해 위에서 보았던 콜백의 예를 코루틴을 사용하여 다시 작성해보자. 

// Dispatchers.Main
suspend fun fetchDocs() {
    // Dispatchers.Main
    val result = get("developer.android.com")
    // Dispatchers.Main
    show(result)
}
// look at this in the next section
suspend fun get(url: String) = withContext(Dispatchers.IO){/*...*/}

코루틴은 코틀린이 위 코드를 실행하고 메인 스레드를 절대로 차단하지 않는 방법을 제공한다. 

 

코루틴은 regular functions에 기반하여 빌드한다. call과 return 이외에도 suspend와 resume을 추가한다. 

  • suspend - 코루틴 실행을 일시 중지하고 모든 로컬 변수를 저장
  • resume - 일시 중단된 코루틴을 이어서 실행

해당 기능은 kotlin의 suspend 키워드에 의해 추가된다. 다른 suspend funciton 에서 호출하거나 새로운 코루틴을 시작하기 위해 launch 와 같은 코루틴 builder를 사용하여야만 호출할 수 있다. 

 

위 예에서 get()은 network request가 시작하기 전에 코루틴을 일시 정지하고 network request가 메인스레드 밖에서 실행되는 것을 책임진다. 그 후 , network request가 완료되고 콜백을 통해 메인스레드에 알리는 대신에 단순히 멈추었던 코루틴을 다시 시작한다. 

 

코루틴이 중단될 때 Current Stack Frame(코틀린에서 function의 실행과 변수를 추적하기 위해 사용)을 복사하여 저장한다. Resume일 때 Stack Frame이 저장 된 위치에서 다시 복사하고 다시 실행된다. 메인 스레드의 모든 코루틴이 일시 중단되면 메인 스레드가 자유롭게 화면을 업데이트하고 사용자 이벤트를 처리할 수 있다. 

 

Main-safety with coroutines

코틀린 코루틴에선 suspend function이 항상 메인 스레드에서 호출되기 때문에 안전하다.  Network request, parsing JSON, read/write form database, 큰 리스트의 반복작업 등 느리게 수행 되거나 사용자가 볼 수있는 "jank"를 유발할 가능성을 가지고 있으므로 메인 스레드를 벗어나야 한다.  

 

suspend를 사용한다고 해서 코틀린에게 다른 스레드에서 함수를 실행하라는 의미는 아니다.

코루틴은 메인 스레드에서 실행되며, suspend는 background를 의미하지 않는다. 

 

Main-safety에 비해 너무 느린 function을 만드려면 코틀린 코루틴에게 Default 혹은 IO dispatcher를 수행하도록 지시하면 된다. 코틀린에서 모든 코루틴이 메인 스레드에서 실행 될 때에도 반드시 Dispatcher가 있어야한다. 코루틴은 스스로를 정지시킬 수 있고, dispatcher는 그것들을 다시 시작할 방법을 알고있다. 

 

코틀린은 다음과 같은 3개의 dispatcher를 지원한다. 

더보기

+-----------------------------------+
| Dispatchers.
Main |
+-----------------------------------+

| Main thread on Android, interact |
| with the UI and perform light |
| work |
+-----------------------------------+
| - Calling suspend functions |
| - Call UI functions |
| - Updating LiveData |
+-----------------------------------+

+-----------------------------------+
| Dispatchers.
IO |
+-----------------------------------+

| Optimized for disk and network IO |
| off the main thread |
+-----------------------------------+
| - Database* |
| - Reading/writing files |
| - Networking** |
+-----------------------------------+

+-----------------------------------+
| Dispatchers.
Default |
+-----------------------------------+

| Optimized for CPU intensive work |
| off the main thread |
+-----------------------------------+
| - Sorting a list |
| - Parsing JSON |
| - DiffUtils |
8+-----------------------------------+

* Room은 RxJava혹은 LiveData를 사용하면 main-safety가 자동으로 제공된다. 

 

** Retrofit, Valley와 같은 네트워크 라이브러리는 자체 스레드를 관리하며, 코루틴과 함께 사용할 경우 코드에 명시적인 main-safety를 요구하지 않는다. 

 

위 예제를 이어 Dispatcher를 사용하여 get() 함수를 정의해보자. get() 안에서 withContext(Dispatchers.IO)를 호출하여 블록을 생성한다. 해당 블록 내부 코드는 항상 Dispatchers.IO에서 실행 될 것이다. withContext() 자체가 일시 중단 기능이 있어서 코루틴을 사용하여 main-safety를 제공할 것이다. 

// Dispatchers.Main
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
        /* perform blocking network IO here */
    }
    // Dispatchers.Main

withContext()를 사용하면 결과 반환을 위해 콜백을 사용하지 않고 코드 라인이 실행되는 스레드를 제어 할 수 있으며, read Database 및 network request와 같이 매우 작은 기능에 적용할 수 있다. 따라서 withContext()를 사용하여 모든 기능이 메인을 포함한 모든 dispatcher에서 호출될 수 있도록 하는 것이 좋은 방법이며, 이렇게 되면 호출자는 해당 기능을 실행하기 위해 어떤 스레드가 필요한지 생각할 필요가 없다. 

 

위 예제에서 fetchDoc()은 메인 스레드에서 실행되지만 백그라운드에서 실행되는 get()을 안전하게 호출할 수 있는 것이다. 코루틴은 suspend와 resume을 지원하므로 withContext()블록이 완료되는 순간 결과와 함께 코루틴이 재개된다. 

 

suspend functions은 메인 스레드에서 호출하기에 안전하다.