[Kotlin] 동기, 비동기, 예외처리

정상준·2023년 2월 6일
0

kotlin

목록 보기
25/26
post-thumbnail
post-custom-banner

📚 동기 코드(synchronous)

동기 코드에서는 한 번에 하나의 task만 진행합니다. 즉 하나의 코드가 끝나야 다음 코드가 실행됩니다.

ex)

fun main() {
    println("Weather forecast")
    println("Sunny")
}

동기 함수는 task가 완전히 완료된 경우에만 반환됩니다. 따라서 main()의 마지막 print 문이 실행된 후에 모든 작업이 완료됩니다. main() 함수가 반환되고 프로그램이 종료됩니다.

✏️ Add a delay

기상청 서버에서 날씨를 가져온다고 가정해보겠습니다. 서버에 날씨를 요청해 응답을 받는데 시간이 걸리기 때문에 코루틴의 정지 함수 delay를 사용하도록 하겠습니다.

import kotlinx.coroutines.*

fun main() {
    runBlocking{
    println("Weather forecast")
    delay(1000)
    println("Sunny")
    }
}

runBlocking 함수는 동기식으로 람다식 안에 있는 모든 작업이 완료되어야 함수를 반환합니다.

출력이 이전과 동일하며 여전히 동기식입니다.

코루틴에서 코는 협력이라는 의미를 가지고 있으며 위의 코드를 예로들면 delay()를 만나 대기할 때 그동안 다른 작업을 할 수 있음을 의미합니다.

✏️ Suspending functions

날씨를 가져오는 코드를 함수로 나누는 리팩토링을 하겠습니다.

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()은 동기식이며 본문의 각 호출은 순차적으로 이루어집니다. 이러한 정지 함수는 하나씩 차례로 실행됩니다.

📚 비동기 코드(synchronous)

✏️ launch()

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!가 출력된다는 것입니다.

✏️ async()

실제 현업에서는 코루틴으로 실행하는 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!

각 코루틴이 완료되면 값이 반환되었습니다.

✏️ 병렬 분해(Parallel Decomposition)

병렬 분해는 문제를 병렬로 해결할 수 있는 더 작은 하위 태스크로 세분화하는 것입니다. 하위 태스크의 결과가 준비되면 최종 결과로 결합할 수 있습니다.

날씨를 가져오는 새로운 코루틴 함수 선언

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 exceptions

코드의 특정 부분에서 예외가 발생할 수 있다는 것을 알고 있으면 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()에 원하던 온도는 나오지 않았지만 사용자에게 적절한 에러메시지를 보여줌으로써 앱이 충돌되는 상황보단 좋게 만들었습니다.

✏️ Cancellation

예외와 유사한 주제로는 코루틴 취소가 있습니다.
위의 코드의 연속으로 사용자가 온도 값을 보고 싶지 않다고 가정해보겠습니다. 그들은 일기예보만 알고 싶습니다. 그러므로 온도를 가져오는 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!
profile
안드로이드개발자
post-custom-banner

0개의 댓글