반갑습니다!

[Kotlin] Composing Suspending Functions 본문

Kotlin

[Kotlin] Composing Suspending Functions

김덜덜이 2020. 7. 5. 02:18

이번엔 suspend 함수들을 어떻게 활용해야할지 알아보자

Sequential by default

네트워크로부터 데이터를 전송받거나 복잡한 연산을 하는 등의 비동기 처리가 필요한 작업을 수행한다고 가정해보자. 그리고 그러한 비동기 작업들을 순차적으로 수행하고 싶다면 어떻게 해야할까?

코루틴은 기본적으로 순차적으로 실행된다. 그렇기에 비동기 작업을 순차적으로 실행하는 경우 아래와 같이 코드를 작성할 수 있다.

import kotlinx.coroutines.*
import kotlin.system.*

fun main() = runBlocking<Unit> {
    val time = measureTimeMillis {
        val one = doSomethingUsefulOne()
        val two = doSomethingUsefulTwo()
        println("The answer is ${one + two}")
    }
    println("Completed in $time ms")    
}

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // 무엇인가 유용한 작업을 한다고 가정
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // 마찬가지로 유용한 작업을 한다고 가정
    return 29
}

/* 실행결과
The answer is 42
Completed in 2014 ms
*/

Concurrent using async

이번엔 동일한 작업들을 순차적 실행이 아니라 비동기 실행으로 처리되게 하려고 한다. 위에서도 언급했듯이 코루틴은 순차 실행을 기본으로 한다. 따라서 각각의 작업을 동시에 실행하기 위해서는 코루틴 빌더async 를 사용하면 된다.

import kotlinx.coroutines.*
import kotlin.system.*

fun main() = runBlocking<Unit> {
    val time = measureTimeMillis {
        val one = async { doSomethingUsefulOne() }
        val two = async { doSomethingUsefulTwo() }
        println("The answer is ${one.await() + two.await()}")
    }
    println("Completed in $time ms")    
}

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // 무엇인가 유용한 작업을 한다고 가정
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // 마찬가지로 유용한 작업을 한다고 가정
    return 29
}

/* 실행결과
The answer is 42
Completed in 1023 ms
*/

실행결과를 보면 약 1초만에 두 작업이 모두 종료되었음을 알 수 있다. 그리고 await() 라는 함수가 등장했는데, 이는 async 내의 동작이 끝날 때까지 기다리는 함수이다. launch 함수를 실행하면 Job 객체를 반환해준다는 사실은 이전의 예제를 통해 학습하였다. async 함수는 Job 객체를 상속받은 Deferred 객체를 반환받는다. launch 는 결과값을 반환받지 못하지만 async 는 반환받은 객체에 await()함수를 호출해 결과값을 얻을 수 있다. 그리고 Deferred 또한 Job 객체의 일종이므로 필요한 경우 취소시킬 수 있다.

Lazily started async

지금까지 코루틴 실행 예제들은 해당 라인이 실행되면 바로 코루틴이 동작했었다. 이번엔 사용자가 지정한 순간에 코루틴을 실행시켜보자. 이 때는 async 함수에 CoroutineStart.LAZY 를 매개변수로 넘겨주면 된다. 이렇게 하면 start() 또는 await() 를 호출해야 코루틴이 실행된다.

import kotlinx.coroutines.*
import kotlin.system.*

fun main() = runBlocking<Unit> {
    val time = measureTimeMillis {
        val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
        val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }
        // some computation
        one.start() // one 실행
        two.start() // two 실행
        println("The answer is ${one.await() + two.await()}")
    }
    println("Completed in $time ms")    
}

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L)
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L)
    return 29
}

/* 실행결과
The answer is 42
Completed in 1026 ms
*/

위의 예제에서 start() 함수들을 지우면 약 2초의 시간이 걸린다는 것을 알 수 있다.

Async-style functions

이번엔 위에서 선언했던 doSomethingUsefulOne() 과 doSomethingUsefulTwo() 를 비동기 함수로 선언해보았다.

// somethingUsefulOneAsync의 실행결과는 Deferred 객체가 된다.
fun somethingUsefulOneAsync() = GlobalScope.async {
    doSomethingUsefulOne()
}

// somethingUsefulTwoAsync의 실행결과는 Deferred 객체가 된다.
fun somethingUsefulTwoAsync() = GlobalScope.async {
    doSomethingUsefulTwo()
}

이런식으로 코드를 작성하면 somethingusefulOneAsync() 와 somethingusefulTwoAsync() 는 suspend 함수가 아니기 때문에 어디서든 호출할 수 있다는 큰 장점이 생긴다. 이 함수들을 사용해서 이전의 예제처럼 코드를 작성하면 다음과 같다.

import kotlinx.coroutines.*
import kotlin.system.*

// runBlocking이 아닌 main 함수에서 실행할 수 있다.
fun main() {
    val time = measureTimeMillis {
        val one = somethingUsefulOneAsync()
        val two = somethingUsefulTwoAsync()
        // 코루틴 결과를 기다리기위해 runBlocking 사용
        runBlocking {
            println("The answer is ${one.await() + two.await()}")
        }
    }
    println("Completed in $time ms")
}

fun somethingUsefulOneAsync() = GlobalScope.async {
    doSomethingUsefulOne()
}

fun somethingUsefulTwoAsync() = GlobalScope.async {
    doSomethingUsefulTwo()
}

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L)
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L)
    return 29
}

/* 실행결과
The answer is 42
Completed in 1126 ms
*/

Coroutine Scope 밖에서도 비동기 처리를 손쉽게 할 수 있다는 점에서 굉장히 편리해보인다. 하지만 이러한 스타일의 코드를 사용하지 말 것을 강력하게 권장하고 있다. 이유는 다음의 예제를 살펴보면서 알아보도록 하자.

import kotlinx.coroutines.*
import kotlin.system.*

// runBlocking이 아닌 main 함수에서 실행할 수 있다.
fun main() {
      try{
        val time = measureTimeMillis {
            val one = somethingUsefulOneAsync()
            val two = somethingUsefulTwoAsync()

            throw Exception("error!!!")

            // 코루틴 결과를 기다리기위해 runBlocking 사용
            runBlocking {
                println("The answer is ${one.await() + two.await()}")
            }
        }
        println("Completed in $time ms")
    } catch (e: Exception) {}

    runBlocking {
        delay(8000)
    }
}

fun somethingUsefulOneAsync() = GlobalScope.async {
      println("start, somethingUsefulOneAsync")
    val result = doSomethingUsefulOne()
    println("end, somethingUsefulOneAsync")
    result
}

fun somethingUsefulTwoAsync() = GlobalScope.async {
      println("start, somethingUsefulTwoAsync")
    val result = doSomethingUsefulTwo()
       println("end, somethingUsefulTwoAsync")
    result
}

suspend fun doSomethingUsefulOne(): Int {
    delay(3000L)
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(3000L)
    return 29
}

/* 실행결과
exception catch!
start, somethingUsefulOneAsync
start, somethingUsefulTwoAsync
end, somethingUsefulOneAsync
end, somethingUsefulTwoAsync
*/

위의 예제에서는 의도적으로 예외를 발생시켜서 코루틴을 종료시키려고 했지만 정상적으로 작동하지 않는다는 것을 알 수 있다. 이는 두 개의 코루틴이 GlobalScope 에서 실행되었기 때문에 예외가 발생하더라도 백그라운드에서 코루틴이 실행된 것이다.

Structured concurrency with async

위의 예제에서 함수를 호출하기는 편해졌지만 오히려 코틀린을 다루기 어려워졌다.

import kotlinx.coroutines.*
import kotlin.system.*

fun main() = runBlocking<Unit> {
    val time = measureTimeMillis {
        println("The answer is ${concurrentSum()}")
    }
    println("Completed in $time ms")    
}

suspend fun concurrentSum(): Int = coroutineScope {
    val one = async { doSomethingUsefulOne() }
    val two = async { doSomethingUsefulTwo() }
    one.await() + two.await()
}

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L)
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L)
    return 29
}

/* 실행결과
The answer is 42
Completed in 1028 ms
*/

다음과 같이 구조적으로 코드를 작성 하면 예외가 발생하면 모든 코루틴이 중지될 것이고, 결과적으로 코루틴을 다루기 한결 수월해질 것이다.

import kotlinx.coroutines.*
import kotlin.system.*

fun main() = runBlocking<Unit> {
    try{
        val time = measureTimeMillis {
            println("The answer is ${concurrentSum()}")
        }
        println("Completed in $time ms")    
    } catch (e: Exception) {
        println("Error!")
    }

    runBlocking {
        delay(8000)
    }
}

suspend fun concurrentSum(): Int = coroutineScope {
    val one = async { doSomethingUsefulOne() }
    val two = async { doSomethingUsefulTwo() }

    delay(10)

    throw Exception()

    one.await() + two.await()
}

suspend fun doSomethingUsefulOne(): Int {
    println("start, doSomethingUsefulOne")
    delay(3000L)
    println("end, doSomethingUsefulOne")
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    println("start, doSomethingUsefulTwo")
    delay(3000L)
    println("end, doSomethingUsefulTwo")
    return 29
}

/* 실행결과
start, doSomethingUsefulOne
start, doSomethingUsefulTwo
Error!
*/

실제로 코드를 작성해보면 예외가 발생하면 코루틴이 종료되는 것을 알 수 있다. 이번엔 다른 예제를 살펴보자.

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    try {
        failedConcurrentSum()
    } catch(e: ArithmeticException) {
        println("Computation failed with ArithmeticException")
    }
}

suspend fun failedConcurrentSum(): Int = coroutineScope {
    val one = async<Int> { 
        try {
            delay(Long.MAX_VALUE) // 아주 오랜 작업을 실행한다고 가정
            42
        } finally {
            println("First child was cancelled")
        }
    }
    val two = async<Int> { 
        println("Second child throws an exception")
        throw ArithmeticException()
    }
    one.await() + two.await()
}

/* 실행결과
Second child throws an exception
First child was cancelled
Computation failed with ArithmeticException
*/

결과를 확인해보면 구조화된 형태로 코루틴을 구성하면 에러가 전파된다는 것을 알 수 있다.

'Kotlin' 카테고리의 다른 글

[Kotlin] Coroutine Context and Dispatchers  (0) 2020.07.05
[Kotlin] Principle of Coroutine  (0) 2020.07.05
[Kotlin] Cancellation and Timeouts  (0) 2020.07.04
[Kotlin] Coroutine Basics  (0) 2020.07.04
[Kotlin] Coroutine Guide  (0) 2020.07.04