동기 코드에서는 한 번에 하나의 task만 진행합니다. 즉 하나의 코드가 끝나야 다음 코드가 실행됩니다.
ex)
fun main() {
println("Weather forecast")
println("Sunny")
}
동기 함수는 task가 완전히 완료된 경우에만 반환됩니다. 따라서 main()의 마지막 print 문이 실행된 후에 모든 작업이 완료됩니다. main() 함수가 반환되고 프로그램이 종료됩니다.
기상청 서버에서 날씨를 가져온다고 가정해보겠습니다. 서버에 날씨를 요청해 응답을 받는데 시간이 걸리기 때문에 코루틴의 정지 함수 delay를 사용하도록 하겠습니다.
import kotlinx.coroutines.*
fun main() {
runBlocking{
println("Weather forecast")
delay(1000)
println("Sunny")
}
}
runBlocking 함수는 동기식으로 람다식 안에 있는 모든 작업이 완료되어야 함수를 반환합니다.
출력이 이전과 동일하며 여전히 동기식입니다.
코루틴에서 코는 협력이라는 의미를 가지고 있으며 위의 코드를 예로들면 delay()를 만나 대기할 때 그동안 다른 작업을 할 수 있음을 의미합니다.
날씨를 가져오는 코드를 함수로 나누는 리팩토링을 하겠습니다.
import kotlinx.coroutines.*
fun main() {
runBlocking {
println("Weather forecast")
printForecast()
}
}
suspend fun printForecast() {
delay(1000)
println("Sunny")
}
정지 함수는 코루틴이나 다른 정지 함수에서만 호출할 수 있으므로 printForecast()를 suspend 함수로 정의합니다.
정지 함수에는 정지 지점을 0개 이상 포함할 수 있습니다. 정지 지점은 함수 내에서 함수 실행을 정지할 수 있는 위치입니다. 실행이 다시 시작되면 코드에서 마지막에 중단한 지점부터 다시 시작되어 함수의 나머지 부분이 진행됩니다.
이번엔 기온을 출력하는 새로운 정지함수를 추가하겠습니다.
import kotlinx.coroutines.*
fun main() {
runBlocking {
println("Weather forecast")
printForecast()
printTemperature()
}
}
suspend fun printForecast() {
delay(1000)
println("Sunny")
}
suspend fun printTemperature() {
delay(1000)
println("30\u00b0C")
}
이 코드에서 코루틴은 먼저 printForecast() 정지 함수의 지연으로 인해 정지되었다가 1초의 지연 후에 다시 시작됩니다. Sunny 텍스트가 출력됩니다. printForecast() 함수가 호출자에 반환됩니다.
그런 다음 printTemperature() 함수가 호출됩니다. 이 코루틴은 delay() 호출에 도달하면 정지되었다가 1초 후에 다시 시작된 후 온도 값을 출력하고 종료됩니다. printTemperature() 함수가 모든 작업을 완료하고 반환됩니다.
runBlocking() 본문에 실행할 추가 태스크가 없으므로 runBlocking() 함수가 반환되고 프로그램이 종료됩니다.
runBlocking()은 동기식이며 본문의 각 호출은 순차적으로 이루어집니다. 이러한 정지 함수는 하나씩 차례로 실행됩니다.
Task를 동시에 실행하려면 동시에 여러 코루틴이 진행될 수 있도록 코드에 launch() 함수를 추가합니다.
import kotlinx.coroutines.*
fun main() {
runBlocking {
println("Weather forecast")
launch{
printForecast()
}
launch{
printTemperature()
}
}
}
suspend fun printForecast() {
delay(1000)
println("Sunny")
}
suspend fun printTemperature() {
delay(1000)
println("30\u00b0C")
}
동기 코드와 출력은 똑같지만 실행 시간은 더 빨라졌습니다.
이전에는 printForecast() 정지 함수가 완전히 완료될 때까지 기다려야 printTemperature() 함수로 이동할 수 있었습니다. 이제 printForecast()와 printTemperature()가 별개의 코루틴에 있으므로 동시에 실행할 수 있습니다.
import kotlinx.coroutines.*
fun main() {
runBlocking {
println("Weather forecast")
launch{
printForecast()
}
launch{
printTemperature()
}
println("Have a good day!")
}
}
suspend fun printForecast() {
delay(1000)
println("Sunny")
}
suspend fun printTemperature() {
delay(1000)
println("30\u00b0C")
}
Weather forecast
Have a good day!
Sunny
30°C
이 출력에서 printForecast() 및 printTemperature()의 새 코루틴 두 개가 실행된 후 Have a good day!를 출력하는 다음 명령을 진행할 수 있음을 볼 수 있습니다. 이는 launch()의 'fire-and-forget' 특성을 보여줍니다. 다시 말해 launch()로 새 코루틴을 실행한 후 작업이 언제 완료될지 걱정할 필요가 없습니다.
하지만 위의 코드의 문제는 코루틴이 끝나기 전에 다음 함수가 실행되어 Have a good day!가 출력된다는 것입니다.
실제 현업에서는 코루틴으로 실행하는 task가 얼마나 걸릴지 예상할 수 없습니다. 두 task가 완료되면 통합하여 날씨를 보고하려는 경우 lauch()는 옳바르지 않습니다. 여기선 async()가 옳바릅니다.
즉 코루틴이 완료되는 시점에 관심이 있고 코루틴의 반환 값이 필요하다면 코루틴 라이브러리의 async() 함수를 사용합니다.
async() 함수는 준비가 다 된다면 Deferred형의 객체를 반환합니다.
import kotlinx.coroutines.*
fun main() {
runBlocking {
println("Weather forecast")
val forecast: Deferred<String> = async {
getForecast()
}
val temperature: Deferred<String> = async {
getTemperature()
}
println("${forecast.await()} ${temperature.await()}")
println("Have a good day!")
}
}
suspend fun getForecast(): String {
delay(1000)
return "Sunny"
}
suspend fun getTemperature(): String {
delay(1000)
return "30\u00b0C"
}
Weather forecast
Sunny 30°C
Have a good day!
각 코루틴이 완료되면 값이 반환되었습니다.
병렬 분해는 문제를 병렬로 해결할 수 있는 더 작은 하위 태스크로 세분화하는 것입니다. 하위 태스크의 결과가 준비되면 최종 결과로 결합할 수 있습니다.
날씨를 가져오는 새로운 코루틴 함수 선언
suspend fun getWeatherReport() = coroutineScope {
val forecast = async { getForecast() }
val temperature = async { getTemperature() }
"${forecast.await()} ${temperature.await()}"
}
전체코드
import kotlinx.coroutines.*
fun main() {
runBlocking {
println("Weather forecast")
println(getWeatherReport())
println("Have a good day!")
}
}
suspend fun getWeatherReport() = coroutineScope {
val forecast = async { getForecast() }
val temperature = async { getTemperature() }
"${forecast.await()} ${temperature.await()}"
}
suspend fun getForecast(): String {
delay(1000)
return "Sunny"
}
suspend fun getTemperature(): String {
delay(1000)
return "30\u00b0C"
}
예외는 코드 실행 중 발생하는 예기치 않은 이벤트입니다.
다음은 0으로 나눗셈을 했기 때문에 예외가 발생합니다.
fun main() {
val numberOfPeople = 0
val numberOfPizzas = 20
println("Slices per person: ${numberOfPizzas / numberOfPeople}")
}
위의 코드에선 단순히 numberOfPeople을 0이 아닌 수로 하면 되지만 코드가 복잡해지면 모든 예외를 예상하고 방지할 수 없습니다.
위에서 계속 진행했던 코드에 throw AssertionError("Temperature is invalid") 예외를 던져보겠습니다.
import kotlinx.coroutines.*
fun main() {
runBlocking {
println("Weather forecast")
println(getWeatherReport())
println("Have a good day!")
}
}
suspend fun getWeatherReport() = coroutineScope {
val forecast = async { getForecast() }
val temperature = async { getTemperature() }
"${forecast.await()} ${temperature.await()}"
}
suspend fun getForecast(): String {
delay(500)
throw AssertionError("Temperature is invalid")
return "Sunny"
}
suspend fun getTemperature(): String {
delay(1000)
return "30\u00b0C"
}
Weather forecast
Exception in thread "main" java.lang.AssertionError: Temperature is invalid
at FileKt.getForecast (File.kt:19)
at FileKt$getForecast$1.invokeSuspend (File.kt:-1)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33)
위와 같은 에러를 이해하기 위해선 코루틴의 부모-자식 관계가 있다는 것을 알아야합니다. 부모 코루틴에서 자식 코루틴을 실행할 수 있습니다. 위의 코드에선 getTemperature()와 getForecast()가 같은 부모 코루틴을 갖고 있습니다. 여기서 만약 둘 중 하나에 예외가 발생한다면 부모에게 알려주고 부모 코루틴에선 자식 코루틴 모두를 취소시킵니다. 마지막으로 부모에게 올라간 예외를 던지며 마무리합니다.
코드의 특정 부분에서 예외가 발생할 수 있다는 것을 알고 있으면 try-catch문으로 묶을 수 있습니다. 그렇게 한다면 사용자에게 유용한 오류 메시지를 표시하는 등 앱에서 예외를 적절히 처리할 수 있습니다.
import kotlinx.coroutines.*
fun main() {
runBlocking {
println("Weather forecast")
try{
println(getWeatherReport())
} catch (e: AssertionError){
println("Caught exception in runBlocking(): $e")
println("Report unavailable at this time")
}
println("Have a good day!")
}
}
suspend fun getWeatherReport() = coroutineScope {
val forecast = async { getForecast() }
val temperature = async { getTemperature() }
"${forecast.await()} ${temperature.await()}"
}
suspend fun getForecast(): String {
delay(1000)
return "Sunny"
}
suspend fun getTemperature(): String {
delay(500)
throw AssertionError("Temperature is invalid")
return "30\u00b0C"
}
Weather forecast
Caught exception in runBlocking(): java.lang.AssertionError: Temperature is invalid
Report unavailable at this time
Have a good day!
getTemperature() 출력에서 예외를 throw하는 것을 관찰할 수 있습니다. 이 동작은 getTemperature()를 실패하면 같은 부모를 갖는 getForecast()도 실행되지 않는 것을 볼 수 있습니다.
하지만 만약 try-catch문의 위치를 이동한다면 getTemperature()가 실행되지 않더라도 getForecast()는 실행될 수 있습니다.
import kotlinx.coroutines.*
fun main() {
runBlocking {
println("Weather forecast")
println(getWeatherReport())
println("Have a good day!")
}
}
suspend fun getWeatherReport() = coroutineScope {
val forecast = async { getForecast() }
val temperature = async {
try {
getTemperature()
} catch (e: AssertionError) {
println("Caught exception $e")
"{ No temperature found }"
}
}
"${forecast.await()} ${temperature.await()}"
}
suspend fun getForecast(): String {
delay(1000)
return "Sunny"
}
suspend fun getTemperature(): String {
delay(500)
throw AssertionError("Temperature is invalid")
return "30\u00b0C"
}
Weather forecast
Caught exception java.lang.AssertionError: Temperature is invalid
Sunny { No temperature found }
Have a good day!
이렇게 함으로써 getForecase()는 실행이 되며 getTemperature()에 원하던 온도는 나오지 않았지만 사용자에게 적절한 에러메시지를 보여줌으로써 앱이 충돌되는 상황보단 좋게 만들었습니다.
예외와 유사한 주제로는 코루틴 취소가 있습니다.
위의 코드의 연속으로 사용자가 온도 값을 보고 싶지 않다고 가정해보겠습니다. 그들은 일기예보만 알고 싶습니다. 그러므로 온도를 가져오는 getTemperature()를 Cancellation 해보도록 하겠습니다.
import kotlinx.coroutines.*
fun main() {
runBlocking {
println("Weather forecast")
println(getWeatherReport())
println("Have a good day!")
}
}
suspend fun getWeatherReport() = coroutineScope {
val forecast = async { getForecast() }
val temperature = async { getTemperature() }
delay(200)
temperature.cancel()
"${forecast.await()}"
}
suspend fun getForecast(): String {
delay(1000)
return "Sunny"
}
suspend fun getTemperature(): String {
delay(500)
throw AssertionError("Temperature is invalid")
return "30\u00b0C"
}
Weather forecast
Sunny
Have a good day!