코틀린 코루틴 가이드 3 — Composing Suspending Functions

김종식
6 min readFeb 6, 2021

--

(공식문서 바로가기)

Composing Suspending Functions

이 장에서는 중단함수에 대한 다양한 접근 방법을 다룹니다.

Sequencial by default

원격 서비스 호출이나 복잡한 계산을 담당해주는 유용한 중단 함수 (suspending function)가 있다고 가정 해 봅시다. 예제코드에서 두 가지 함수는 결과를 달성하기 위하여 지연을 둬서 유용한 함수인 것 처럼 작성 하였습니다.

이러한 상황에서 두 함수를 순차적으로 호출해야 한다면, 예를들어 번째 doSomethingUsefulOne의 결과와 두 번째 doSomethingUsefulTwo의 결과의 합을 계산하려면 어떻게 해야할 까요? 실제로 우리는 첫 번째 함수의 결과를 이용해 두 번째 함수를 호출하거나 그 호출 방식을 결정해야 할 경우 이런 상황이 발생합니다.

코루틴의 경우 일반적으로 작성된 코드들 처럼 기본적으로 순차적으로 수행됩니다. 예제 코드는 두 가지 일시 중단 기능을 실행하는 데 걸리는 총 시간을 측정하여 보여줍니다. (순차적으로 실행되기 때문에, 중단 함수 각각의 수행 시간의 합 만큼의 시간이 소모되었음을 확인할 수 있습니다.)

The answer is 42 
Completed in 2017 ms

Concurrent using async

만약 doSomethingUsefulOne 함수와 doSomethingUsefulTwo 함수 사이에 서로 의존성이 없고 동시에 실행함으로써 더 빨리 원하는 결과(= 각 함수의 실행 결과)를 먼저 얻으려면 어떻게 해야할까요? async 가 도움이 될 수 있습니다.

개념적으로 async launch 와 같습니다. 차이점은 launch 의 경우 Job을 반환하고 결과값을 제공하지 않는 반면, async는 추후 결과를 제공한다는 것이 약속된, 경량화된 넌블로킹 미래(non-blocking future) 인 Deferred 를 반환합니다. .await() 를 사용하여 최종 결과를 나중에 전달 받을 수 있지만, Deferred 또한 Job 타입이므로 만약 필요할 경우 취소를 요청할 수 있습니다. (= Deferred Job을 구현하고 있는 인터페이스로 결과값을 가지고있는 Job 입니다.)

예제에서, 두 개의 코루틴은 비동기적으로 실행되기 때문에 두 배 더 빠릅니다. 코루틴의 동시성은 항상 명백하다는 것을 유의해야 합니다.

Lazily started async

추가적으로, async 함수는 ‘start’ 파라미터로 CoroutineStart.LAZY 을 설정하여
지연 실행이 가능합니다. (=비동기 처리 시작 요청이 반드시 되어야 실행됩니다.) 이 모드(LAZY) 에서는, 코루틴의 결과를 필요로 하는 시점에 await() 를 호출하거나, start() 를 통해 지연 시작이 가능합니다.

The answer is 42 
Completed in 1017 ms

따라서, 여기 두 코루틴은 앞의 예와 같이 바로 실행되지는 않지만, start() 호출은 개발자가 원하는 시점에 가능합니다. 먼저 ‘one’ 을 실행시키고, 다음에 ‘two’ 를 실행시키며, 각각의 코루틴 결과가 종료될 때 까지 대기하게 됩니다.

만약 위 코드에서 먼저 start() 를 시작하지 않고 println 에서 await() 만 호출할 경우, 순차적으로 동작될 것이므로 이는 lazy을 설계한 의도대로 사용하지 않았다고 할 수 있습니다. async(start = CoroutineStart.LAZY) 의 경우 코루틴의 수행 결과를 중단함수에서 전달하는 경우가 아니라면 lazy 로 사용을 대체할 수 있습니다.

Async-style functions

우리는 이전에 작성된 doSomethingUsefulOne(), doSomethingUsefulTwo() 함수를 명시적으로 GlobalScopeasync() 코루틴빌더를 활용하여 비동기스타일의 함수로 정의가 가능합니다. 이러한 함수의 이름을 "...Async" 접미사를 지정하여 해당 함수는 지연처리된 결과를 사용하기 위함을 관례적으로 표현하여 강조할 수 있습니다.

이러한 ‘xxxAsync’ 함수는 중단함수가 아닙니다. 따라서, 어디에서든지 사용이 가능합니다. 하지만 이 함수들의 실행은 항상 비동기적으로 실행됨을 의미하고 있습니다.

다음 예제는 이 함수들이 코루틴이 아닌 곳에서 사용하는 예제입니다.

이와같이 비동기 함수구성을 프로그래밍 스타일 예시로 제공한 이유는 단지 다른 프로그래밍 언어에서 인기있는 스타일이기 때문입니다. 코틀린 코루틴에서는 아래 설명된 내용으로 인해 크게 권장하지 않습니다.

‘val one = somethingUsefulOneAsync()’ 라인과 ‘one.await()’ 사이에 오류가 있어 예외가 발생하여 프로그램이 수행되고 있던 작업이 중단될 경우 어떻게 동작할지 생각해 보십시오. 일반적으로 글로벌 에러핸들러에서는 이 예외를 캐치하여 처리할 수 있지만( try {…} catch(exception) ) 프로그램은 다른 동작을 계속 수행하게 됩니다. somethingUsefulOneAsync() 이 여전히 백그라운드에서 실행되고 있지만, 이 비동기를 실행시킨 루틴은 종료가 되어버렸습니다. 이러한 문제는 잘 구조화된 동시성 코드에서는 발생되지 않습니다.

Structured concurrency with async

async를 활용한 비동기 처리 예제에서(=2번째 예제) doSomethingUsefulOne()doSomethingUsefulTwo() 함수를 각자 비동기적으로 수행하여 각각의 합을 리턴하는 함수를 작성해 봅시다. async() 코루틴 빌더는 CoroutineScope 의 확장함수로 선언 되어있기 때문에 이 빌더를 코루틴 스코프 내에서 호출이 가능하며, 이것은 coroutineScope() 함수가 제공합니다.

이런식으로 구조화되게 코루틴을 별도 함수로 작성한다면, 만약 ‘concurrentSum()’ 코드블록 내 예외가 발생된다면, 해당 스코프 내에서 실행되어진 모든 코루틴은 취소됩니다.

메인 함수의 출력 결과로부터, 여전히 두 작업이 동시적으로 실행되는 것을 확인할 수 있습니다.

The answer is 42
Completed in 1017 ms

코루틴 취소 역시 코루틴 계층을 따라 전파됩니다.

상위 루틴은 자식 루틴 (예제에서는 two)이 취소 혹은 실패됨에 따라 나머지 자식 코루틴이 취소되고 그 후에 부모 코루틴이 취소 되는 것을 확인할 수 있습니다.

Second child throws an exception 
First child was cancelled
Computation failed with ArithmeticException

--

--

김종식
김종식

Written by 김종식

앱 개발자 / 꿈은 축구선수 / 쌍둥이 아빠

No responses yet