안드로이드 앱이 실행되면 메인화면(Main Thread = UI Thread)이 실행되고, 기본적으로 요청을 동기적으로 하나씩 처리한다. Main Thread에선 보통 UI 작업들을 하는데, UI작업 뿐만 아니라 모든 작업들을 동기적으로 처리할 경우 Main Thread가 하는 일이 너무 많아져 앱의 퍼포먼스와 속도가 저하될 수 있다.
하지만 비동기 프로그래밍을 사용하면 많은 작업들을 병렬적으로 처리할 수 있고, 곧 Main Thread의 무거운(ex Network, Database) 작업들을 백그라운드에서 따로 처리할 수 있게 된다. 이렇게되면 Main Thread에서는 UI 처리만 담당하면 되기 때문에, 앱의 퍼포먼스와 속도를 향상시킬 수 있다.
안드로이드에서 비동기 프로그래밍을 구현할 때 사용하는 라이브러리로는 대표적으로 AsyncTask, RxJava, RxKotlin, Coroutine이 있다. 이 중 AsyncTask 는 메모리 누수 문제와 버전별 일관적이지 않은 동작으로 인해 예전에 deprecated 되었고, 대체 라이브러리로 RxJava나 coroutine이 많이 사용되고 있다. 물론 다 훌륭한 라이브러리지만, RxJava 및 RxKotlin에 비해 API가 더 간단하고, 코드를 더 간략하게 짤 수 있는 Coroutine을 사용해보기로 했다. (상대적으로 말해서 그렇지, 물론 coroutine도 쉽지 않다!).
코루틴은 비동기적으로 실행되는 코드를 간소화하기 위해 안드로이드에서 사용할 수 있는 동시 실행 설계 패턴이다. 코루틴은 하나의 작업과 같고, 단일스레드를 사용하기 때문에 가볍다(여러개의 코루틴들이 단일스레드 위에서 동작하기 때문에 같은 수의 스레드를 실행하는 것보다 훨씬 더 가벼움). 따라서 다수의 스레드 관리를 직접 해주지 않아도 되고, 스레드가 중지되지 않고 계속 실행될 수 있기 때문에 성능 향상을 기대할 수 있고 여러 스레드를 사용하는 것보다 리소스를 훨씬 덜 소모한다. 코루틴이 가진 장점들을 더 구체적으로 설명하면 다음과 같다.
가벼움
메모리 누수 방지
Jetpack과의 통합
본격적으로 코루틴을 사용해보기 전, 코루틴의 구성요소에는 어떤것들이 있는지 알아보자. 먼저 코루틴은 구조적 동시성을 위해 반드시 CoroutineScope내에서 실행되어야 한다. CoroutineScope는 말 그대로 코루틴이 실행되는 '범위'다. CoroutineScope는 스코프 내에서 실행된 모든 코루틴들을 추적할 수 있고, 취소할 수도 있다. 만약 스코프가 취소되면, 내부에서 실행중이던 모든 코루틴들도 취소되는 것이다. 하나의 코루틴 스코프 내의 코루틴들은 '순차적으로' 실행된다. CoroutineScope는 용도에 따라 여러개의 종류가 있다.
코루틴 스코프를 생성할 때는 파라미터로 CoroutineContext를 전달해야 한다. CoroutineContext는 코루틴이 실행될 컨텍스트로, 우리는 CoroutineContext를 통해 코루틴의 목적에 맞게 실행될 특정 스레드풀을 지정해줘야 한다.
private val scope by lazy { CoroutineScope(Dispatchers.Main) }
private val scope by lazy { CoroutineScope(Dispatchers.IO) }
생성된 코루틴 스코프에서 작업들을 실행하기 위해선 코루틴 빌더가 필요하다. 코루틴 빌더는 launch{}, async{}, runBlocking{} 2개의 메서드를 사용할 수 있다. 둘 다 코루틴을 실행하지만, 결과를 반환하는가 반환하지 않는가에 차이가 있다.
fun main() {
GlobalScope.launch {
delay(1000L) //suspend 함수
println("World!")
}
println("Hello, ")
Thread.sleep(2000L) //UI가 차단되는 함수
}
출력:
Hello,
World!
fun main() {
GlobalScope.launch {
delay(3000L)
println("World!")
}
println("Hello, ")
Thread.sleep(2000L)
}
출력:
Hello,
fun main () = runBlocking {
val job = GlobalScope.launch {
delay(3000L)
println("World!")
}
println("Hello,")
job.join() //job이 완료될 때까지 기다림
}
출력:
Hello,
World!
fun main() = runBlocking {
val job = launch {
repeat(1000){
println("job: I'm sleeping $i...")
delay(500L)
}
}
delay(1300L)
println("main: I'm tired of waiting!")
job.join()
println("main : quit")
}
출력:
job: I'm sleeping 0...
job: I'm sleeping 1...
job: I'm sleeping 2...
job: I'm sleeping 3...
job: I'm sleeping 4...
...
main: I'm tired of waiting!
main : quit
fun main() = runBlocking {
val job = launch {
repeat(1000){
println("job: I'm sleeping $i...")
delay(500L)
}
}
delay(1300L)
println("main: I'm tired of waiting!")
job.cancel()
job.join()
println("main : quit")
}
출력:
job: I'm sleeping 0...
job: I'm sleeping 1...
job: I'm sleeping 2...
main: I'm tired of waiting!
main : quit
val deferred : Deferred<String> = async {
var i = 0
while (i < 10) {
delay(500)
i++
}
"result"
}
val msg = deferred.await() //결과 반환
println(msg) // 결과가 반환되면 result 출력
Job은 launch{}로 생성된 코루틴의 상태를 관리하는 용도로 사용하고 결과값을 리턴받을 수 없으나, Deferred는 async 블록 내 수행된 결과를 원하는 시점에 반환받을 수 있다는 차이가 있다. 기본적으로 Deferred는 Job을 상속받아 구현되었기 때문에 Job의 기능을 사용할 수 있다.
코루틴은 기본적으로 일시중단이 가능하고, 코루틴의 일시 중단 기능은 코루틴 스코프 내에서만 가능하다. suspend fun은 일시 중단이 가능한 함수로, 코루틴 스코프 또는 suspend fun 함수 내에서만 사용할 수 있다.
coroutineScope.launch {
runTask()
}
...
suspend fun runTask() : String = "completed"
https://www.geeksforgeeks.org/kotlin-coroutines-on-android/
https://www.section.io/engineering-education/comparing-rxkotlin-to-kotlin-coroutines/
StackOverFlow - RxJava vs Coroutines
https://blog.danlew.net/2021/01/28/rxjava-vs-coroutines/
https://developer.android.com/kotlin/coroutines?hl=ko&gclsrc=aw.ds&gclid=CjwKCAjw682TBhATEiwA9crl31Df7c3zT08jFjwYU-DWg4hlj7ii7W88nnN2D9zPRUHBqcakN70z3xoCtRkQAvD_BwE#dependency
Structured Concurrency
https://thgus13.tistory.com/25
https://proandroiddev.com/kotlin-coroutines-in-android-summary-1ed3048f11c3