[Android][Kotlin] 코루틴(Coroutine) 사용해보기
by Yena Choi
Study Note
비동기 처리를 하는 데에는 몇 가지 방법이 있다. Rx
는 다양한 기능을 제공하지만 진입 장벽이 높아 학습에 오랜 시간이 걸린다. 또 오랜 기간 사용되었던 AsyncTask
는 지금은 Deprecated 된데다가 구리다.(내가 본 영상에서는 AsyncTask는 sucks 혹은 shit 로 표현되었다.)
코루틴을 이용하면 비동기스럽지 않게 생긴 코드로, 메모리를 효율적으로 사용하면서 손쉽게 비동기 처리를 할 수 있다. 그래서 간단히 알아보았다.
코루틴, Coroutine?
Coroutine
은 안드로에드에서 백그라운드 스레드에서 코드를 처리할 때 사용하는 하나의 방법이다. 코틀린 언어의 하위 개념인 줄 알았는데, C# 이나 Python, Go 등 다양한 언어에서 이미 지원하고 있는 개념이다. 어쩐지 Koroutine 이 아니더라. 코틀린 공식 페이지에 코루틴에 대한 설명을 다룬 공식 문서가 있다.
Coroutine을 사용하는-즉 백그라운드 태스크가 필요한-대표적인 경우는 아무래도
- 네트워크 리퀘스트 (Retrofit, Volley 등)
- 내부 저장소 접근 (Room, SQLite 등)
정도가 되겠다.
코루틴은 코드가 아주 간단하고, 블록으로 처리를 할 수 있기 때문에 하나의 Request-Response 송수신 후에 또 다른 연속적인(sequential) 작업을 하기 좋게 최적화 되어 있는 것 같다.
Coroutine - Thread 차이
백그라운드 태스크라는 점에서 비슷하게 느껴지지만, Coroutine과 Thread는 개념이 다르다.
- 코루틴이 하나의 실행-종료되어야 하는 일(Job)이라고 한다면,
- 쓰레드는 그 일이 실행되는 곳이다.
따라서 하나의 쓰레드에 여러 개의 코루틴이 동시에 실행될 수 있다.
이미지 출처 - https://www.youtube.com/watch?v=F63mhZk-1-Y
Main Thread 이외에 Sub Thread 가 있다면 이 둘을 동시에 병행 실행하는 개념이다.
코루틴은 Co(협력, 같이) 라는 뜻과 Routine(특정한 일을 실행하기 위한 일련의 명령) 이라는 두 단어의 합성어이다. 하나의 쓰레드가 끝날 때까지 계속되는 것과는 달리 코루틴은 실행 중간에 다른 작업을 하러 갔다가 돌아와서 작업을 다시 할 수 있다. 여러 개의 코루틴을 동시에 시작하는 코드가 있다면 다음 그림과 같이 실행될 것이다.
이미지 출처 - 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