비동기 처리를 하는 데에는 몇 가지 방법이 있다. Rx는 다양한 기능을 제공하지만 진입 장벽이 높아 학습에 오랜 시간이 걸린다. 또 오랜 기간 사용되었던 AsyncTask는 지금은 Deprecated 된데다가 구리다.(내가 본 영상에서는 AsyncTask는 sucks 혹은 shit 로 표현되었다.)

코루틴을 이용하면 비동기스럽지 않게 생긴 코드로, 메모리를 효율적으로 사용하면서 손쉽게 비동기 처리를 할 수 있다. 그래서 간단히 알아보았다.


코루틴, Coroutine?

Coroutine은 안드로에드에서 백그라운드 스레드에서 코드를 처리할 때 사용하는 하나의 방법이다. 코틀린 언어의 하위 개념인 줄 알았는데, C# 이나 Python, Go 등 다양한 언어에서 이미 지원하고 있는 개념이다. 어쩐지 Koroutine 이 아니더라. 코틀린 공식 페이지에 코루틴에 대한 설명을 다룬 공식 문서가 있다.

Coroutine을 사용하는-즉 백그라운드 태스크가 필요한-대표적인 경우는 아무래도

  1. 네트워크 리퀘스트 (Retrofit, Volley 등)
  2. 내부 저장소 접근 (Room, SQLite 등)

정도가 되겠다.

코루틴은 코드가 아주 간단하고, 블록으로 처리를 할 수 있기 때문에 하나의 Request-Response 송수신 후에 또 다른 연속적인(sequential) 작업을 하기 좋게 최적화 되어 있는 것 같다.


Coroutine - Thread 차이

백그라운드 태스크라는 점에서 비슷하게 느껴지지만, Coroutine과 Thread는 개념이 다르다.

  • 코루틴이 하나의 실행-종료되어야 하는 일(Job)이라고 한다면,
  • 쓰레드는 그 일이 실행되는 곳이다.

따라서 하나의 쓰레드에 여러 개의 코루틴이 동시에 실행될 수 있다.

coroutine-thread

이미지 출처 - https://www.youtube.com/watch?v=F63mhZk-1-Y


Main Thread 이외에 Sub Thread 가 있다면 이 둘을 동시에 병행 실행하는 개념이다.

코루틴은 Co(협력, 같이) 라는 뜻과 Routine(특정한 일을 실행하기 위한 일련의 명령) 이라는 두 단어의 합성어이다. 하나의 쓰레드가 끝날 때까지 계속되는 것과는 달리 코루틴은 실행 중간에 다른 작업을 하러 갔다가 돌아와서 작업을 다시 할 수 있다. 여러 개의 코루틴을 동시에 시작하는 코드가 있다면 다음 그림과 같이 실행될 것이다.

coroutine-thread

이미지 출처 - https://www.slideshare.net/BartomiejOsmaek/kotlin-coroutines-the-new-async



시작하기

dependency 추가

gradle 파일에 디펜던시를 추가해준다.

dependencies {
    ...
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.5"
}

최신 버전은 Kotlin Github 에서 확인할 수 있다.


Coroutine Scope

이전 포스트(코틀린 Scope함수 정리)에서 Scope는 범위를 뜻한다는 것을 알았다. Coroutine Scope 는 새로운 코루틴을 생성함과 동시에 실행되어야 할 Job을 그룹핑한다. 그래서 하나의 작업이 끝나고 다른 작업을 호출하다가 실패하게 된다면 전체가 취소 처리 된다.

CoroutineScope(Main).launch {
    // do something
}

CoroutineScope(IO).launch {
    // do something
}

CoroutineScope(Default).launch {
    // do something
}

여기 주석 색깔 왜이래요


코루틴 컨텍스트 CoroutineContext 에는 Main, IO, Default의 세 가지가 있다.

  • Main은 말 그대로 메인 쓰레드에 대한 Context이며 UI 갱신이나 Toast 등의 View 작업에 사용된다.
  • IO는 네트워킹이나 내부 DB 접근 등 백그라운드에서 필요한 작업을 수행할 때 사용된다.
  • Default는 크기가 큰 리스트를 다루거나 필터링을 수행하는 등 무거운 연산이 필요한 작업에 사용된다.


suspend

임의의 api와 통신한 후 성공 여부를 반환받는 getResultFromApi 함수가 있다고 하자.

const val RESULT_OK = "ok"

suspend fun getResultFromApi(): String {
    // do something
    return RESULT_OK
}

이 함수 앞에는 suspend가 붙어 있다. Suspend 함수는 그 함수가 비동기 환경(Asynchronous)에서 사용될 수 있다는 의미를 내포한다. 비동기 함수인 suspend 함수는 다른 suspend 함수, 혹은 코루틴 내에서만 호출할 수 있고, 아닌 곳에서 그냥 호출하려고 하면 warning이 뜨면서 이런 메세지가 나온다.

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


코루틴의 컨텍스트를 사용해 함수를 실행하려면 suspend 를 붙여주어야 한다. suspend를 붙인 함수는 아래와 같이 코루틴 스코프 안에서 실행이 가능하다.


delay

delay는 코루틴에서 정의된 suspend function이다. 즉 코루틴이나 다른 suspend 함수 안에서만 수행될 수 있다.

괄호 안의 ms만큼 실행을 멈춘다. Thread.sleep(1000)와 거의 비슷하게 느껴질 것이다. 하지만 위에서 설명한 것처럼 코루틴은 쓰레드 안에서 돌아가는 하나의 Job이며 그 쓰레드 안에 여러 개의 코루틴이 실행되고 있을 수 있다.

따라서 delay는 코루틴 하나만 멈추게 되지만, Thread.sleep은 해당 쓰레드 안에 있는 코루틴을 다 멈추게 된다. 코루틴 안에서 쓰레드 슬립을 호출하지 않는 편이 좋겠다.


withContext

아래의 코드를 실행하면 크래쉬가 발생한다.

CoroutineScope(IO).launch {
    val resultStr = getResultFromApi() //resultStr = "ok"
    textView.text = resultStr
}

// Crash!


IO context에서 네트워크 통신을 한 것 까지는 좋았지만 텍스트를 설정하는 부분은 메인 쓰레드에서 작업해야 하기 때문이다. 따라서 text를 세팅하는 부분은 Main 에서 실행해준다.

CoroutineScope(IO).launch {
    val resultStr = getResultFromApi() //resultStr = "ok"

    CoroutineScope(Main).launch {
        textView.text = resultStr
    }
}

// ok

코루틴 안에 Main context를 가지는 또다른 코루틴을 생성해 처리해주었다. 하지만 한 눈에 들어오지도 않고 코루틴을 하나 더 생성해야 해서 리소스 낭비가 있다. 이럴 때 사용할 수 있는 것이 바로 withContext 이다.


조금 전의 코드를 withContext를 이용해 바꾸어보았다.

CoroutineScope(IO).launch {
    val resultStr = getResultFromApi() //resultStr = "ok"

    withContext(Main) {
        textView.text = resultStr
    }
}

코루틴은 쓰레드로부터 독립적(Thread independent)이다. MainThread에서 하나의 코루틴을 시작하고, 이것을 다른 SubThread1로 보내고, 또 SubThread2로 보냈다가 다시 MainThread 에서 작업을 하는게 가능하다. 이렇게 컨텍스트 스위칭을 해주는 것이 withContext의 역할이다.


withTimeoutOrNull

네트워크 타임아웃 처리는 withTimeoutOrNull(timeMillis) 를 이용하면 손쉽게 처리할 수 있다. 밀리세컨드를 초과하는 시간이 걸리는 경우 null을 반환한다.

CoroutineScope(IO).launch {
    val resultStr = withTimeoutOrNull(10000) {
        getResultFromApi()
    }

    if (resultStr != null) {
        withContext(Main) {
            textView.text = resultStr
        }
    }
}



References

  • https://www.youtube.com/watch?v=F63mhZk-1-Y
  • https://kotlinlang.org/docs/tutorials/coroutines/coroutines-basic-jvm.html
  • https://wooooooak.github.io/kotlin/2019/08/25/%EC%BD%94%ED%8B%80%EB%A6%B0-%EC%BD%94%EB%A3%A8%ED%8B%B4-%EA%B0%9C%EB%85%90-%EC%9D%B5%ED%9E%88%EA%B8%B0/
  • https://kotlinlang.org/docs/reference/coroutines/coroutines-guide.html