비동기 처리를 하는 데에는 몇 가지 방법이 있다. Rx는 다양한 기능을 제공하지만 진입 장벽이 높아 학습에 오랜 시간이 걸린다.
코루틴을 이용하면 비동기스럽지 않게 생긴 코드로, 메모리를 효율적으로 사용하면서 손쉽게 비동기 처리를 할 수 있다. 그래서 간단히 알아보았다.
# 코루틴(Coroutine)
비동기적으로 실행되는 코드를 간소화하기 위해 Android에서 사용할 수 있는 동시 실행 설계 패턴이다.
코틀린 언어의 하위 개념인 줄 알았는데, C#이나 Python, Go 등 다양한 언어에서 이미 지원하고 있는 개념이다.
Coroutine을 사용하는-즉 백그라운드 태스크가 필요한-대표적인 경우는 아무래도
1. 네트워크 리퀘스트 (Retrofit, Volley 등)
2. 내부 저장소 접근 (Room, SQLite 등)
정도가 되겠다.
# 코루틴(Coroutine) - 스레드(Thread) 차이
백그라운드 태스크라는 점에서 비슷하게 느껴지지만, Coroutine과 Thread는 개념이 다르다.
- 코루틴이 하나의 실행-종료되어야 하는 일(Job)이라고 한다면,
- 스레드는 그 일이 실행되는 곳이다.
따라서 하나의 스레드에 여러 개의 코루틴이 동시에 실행될 수 있다.
Main Thread 이외에 Sub Thread 가 있다면 이 둘을 동시에 병행 실행하는 개념이다.
# 코루틴의 장점
안드로이드 공식 문서에 나와 있는 코루틴의 장점은 아래와 같이 설명하고 있다.
코루틴은 Android의 비동기 프로그래밍에 권장되는 솔루션입니다. 주목할 만한 기능은 다음과 같습니다.
- 경량 : 코루틴을 실행 중인 스레드를 차단하지 않는 정지를 지원하므로 단일 스레드에서 많은 코루틴을 실행할 수 있습니다. 정지는 많은 동시 작업을 지원하면서도 차단보다 메모리를 절약합니다.
- 메모리 누수 감소 : 구조화된 동시 실행을 사용하여 범위 내에서 작업을 실행합니다.
- 기본으로 제공되는 취소 지원 : 실행 중인 코루틴 계층 구조를 통해 자동으로 취소가 전달됩니다.
- Jetpack 통합 : 많은 Jetpack 라이브러리에 코루틴을 완전히 지원하는 확장 프로그램이 포함되어 있습니다. 일부 라이브러리는 구조화된 동시 실행에 사용할 수 있는 자체 코루틴 범위도 제공합니다.
# 코루틴의 키워드
1. 협력형 멀티 태스킹
Co(협력, 같이)라는 뜻과 Routine(특정한 일을 실행하기 위한 일련의 명령)이라는 두 단어의 합성어이고 하나의 스레드가 끝날 때까지 계속되는 것과는 달리 코루틴은 실행 중간에 다른 작업을 하러 갔다가 돌아와서 작업을 다시 할 수 있다.
2. 동시성 프로그래밍 지원
함수를 중간에 빠져나왔다가, 다른 함수에 진입하고, 다시 원점으로 돌아와 멈추었던 부분부터 다시 시작하는 이 특성은 동시성 프로그래밍을 가능하게 한다.
함수를 실행하는 중 딜레이 되는 시간에 다른 함수에 진입해 실행하는 것으로 왔다 갔다 하는 시간이 빨라 마치 사용자가 보기에는 동시에 진행되는 거처럼 보인다
3. 비동기 처리를 쉽게 도와줌
# 코루틴 시작하기
# 의존성(dependency) 추가
dependencies {
...
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.5"
}
최신 버전은 여기에서 확인 가능하다.
# 코루틴 스코프(Coroutine Scope)
Scope는 범위를 뜻한다는 것이다. Coroutine Scope는 새로운 코루틴을 생성함과 동시에 실행되어야 할 Job을 그룹핑한다. 그래서 하나의 작업이 끝나고 다른 작업을 호출하다가 실패하게 된다면 전체가 취소 처리된다.
CoroutineScope(Dispatchers.Main).launch {
// do something
}
CoroutineScope(Dispatchers.IO).launch {
// do something
}
CoroutineScope(Dispatchers.Default).launch {
// do something
}
스코프 안에 Dispatchers는 생략이 가능하다.
코루틴 콘텍스트(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(Dispatchers.IO).launch {
val resultStr = getResultFromApi() //resultStr = "ok"
textView.text = resultStr
}
// Crash!
IO context에서 네트워크 통신을 한 것 까지는 좋았지만 텍스트를 설정하는 부분은 메인 스레드에서 작업해야 하기 때문이다. 따라서 text를 세팅하는 부분은 Main에서 실행해준다.
CoroutineScope(Dispatchers.IO).launch {
val resultStr = getResultFromApi() //resultStr = "ok"
CoroutineScope(Dispatchers.Main).launch {
textView.text = resultStr
}
}
// ok
코루틴 안에 Main context를 가지는 또 다른 코루틴을 생성해 처리해주었다. 하지만 한눈에 들어오지도 않고 코루틴을 하나 더 생성해야 해서 리소스 낭비가 있다. 이럴 때 사용할 수 있는 것이 바로 withContext이다.
조금 전의 코드를 withContext를 이용해 바꾸어보았다.
CoroutineScope(Dispatchers.IO).launch {
val resultStr = getResultFromApi() //resultStr = "ok"
withContext(Dispatchers.Main) {
textView.text = resultStr
}
}
코루틴은 스레드로부터 독립적(Thread independent)이다. MainThread에서 하나의 코루틴을 시작하고, 이것을 다른 SubThread1로 보내고, 또 SubThread2로 보냈다가 다시 MainThread에서 작업을 하는 게 가능하다. 이렇게 콘텍스트 스위칭을 해주는 것이 withContext의 역할이다.
# withTimeoutOrNull
네트워크 타임아웃 처리는 withTimeoutOrNull(timeMillis)를 이용하면 손쉽게 처리할 수 있다. 밀리세컨드를 초과하는 시간이 걸리는 경우 null을 반환한다.
CoroutineScope(Dispatchers.IO).launch {
val resultStr = withTimeoutOrNull(10000) {
getResultFromApi()
}
if (resultStr != null) {
withContext(Dispatchers.Main) {
textView.text = resultStr
}
}
}
참고사이트
https://blog.yena.io/studynote/2020/04/26/Android-Kotlin-Coroutine.html
https://velog.io/@dev_jin/Android%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C%EC%BD%94%EB%A3%A8%ED%8B%B4%EC%9D%98-%EA%B8%B0%EC%B4%88
'안드로이드 공부 노트' 카테고리의 다른 글
Android Jetpack - 5편 ViewModel + DataBinding + LiveData 통합 사용법 (0) | 2022.02.24 |
---|---|
Android Jetpack - 4편 데이터 바인딩(Data Binding)이란? (0) | 2022.02.20 |
[Android] Context란? (0) | 2022.01.29 |
Android Jetpack - 3편 (LiveData) MVVM 패턴을 이용한 사용법 Room + ViewBinding + LiveData 통합 (0) | 2022.01.27 |
안드로이드 Fragment LifeCycle(프래그먼트 생명 주기)알아보기! (0) | 2022.01.23 |