코루틴 공식 가이드 자세히 읽기 — Part 4
공식 가이드 읽기 (4 / 8)
중단 함수의 합성
Korean [English]
(목차로 돌아가기)
기본 순차 실행 (Sequential by default)
우리에게 어떤 유용한 일을 수행하는 두 개의 중단 함수가 다음과 같은 이름으로 정의되어 있다고 가정해 봅시다 (doSomethingUsefulOne, doSomethingUsefulTwo). 유용한 기능이라면 원격 서비스의 호출이나 복잡한 계산을 수행하는 것 들을 예로 들수 있을 것 같습니다. 여기서 우리는 이 중단 함수들이 유용하다고 여길 것이지만 두 함수는 사실 예제를 위해 작성된 일정시간 delay() 함수를 호출하여 지연하는 기능만 있는 함수일 뿐입니다.
이러한 상황에서 만약 우리가 이 두 함수를 순차적으로 호출해야 한다면 어떻게 해야할까요? doSomethingUsefulOne() 호출 후 doSomethingUsefulTwo() 를 호출하고 그 합을 계산하는 것 처럼요. 실제로는 우리가 첫번째 함수의 결과를 이용해 두번째 함수를 호출하거나 그 호출 방식을 결정해야 할 경우 이런 상황이 발생합니다.
위 예제를 보면 일반적인 함수처럼 순차적으로 이 중단 함수들을 호출하였습니다. 이것이 가능한 이유는 코루틴에서 기본적으로 이 중단 함수들이 순차적으로 수행되기 때문입니다.
<결과>
The answer is 42
Completed in 2026 ms
결과에서 총 소요시간을 보면 함수가 순차적으로 수행되었기 때문에 거의 중단 함수 각각의 수행시간의 합 만큼의 시간이 소모 되었음을 알 수 있습니다.
aync 를 이용한 동시 수행 (Concurrent using async)
만약 앞서 예로 들었던 두 중단함수가 서로 의존성이 없고 둘중 어느것으로 부터든 빠른 쪽의 결과를 먼저 수신 하고자 한다면 async { } 빌더를 사용할 수 있습니다.
async { } 코루틴 빌더는 launch { } 빌더와 동일하게 전달된 중단함수 블록을 이용하여 새로운 코루틴을 생성 및 실행하지만 중단함수 블록의 실행 결과를 Deferred<T> 를 통해 전달 받을 수 있다는 점에서 차이가 있습니다. 다시말해 launch { } 빌더는 Job 오브젝트를 반환하여 Job을 취소할 수 있도록 지원하지만 async { } 코루틴 빌더는 Deferred<T> 객체를 반환함으로써 Job의 취소 지원과 함께 await() 함수를 통한 결과값 T도 전달 받을 수 있도록 합니다(결과를 나중에 제공하는 Promise).
내부적으로 launch { } 빌더는 StandaloneCoroutine을 생성하고 async { } 빌더는 DeferredCoroutine<T>을 생성하며 DeferredCoroutine은 Deferred<T>인터페이스를 통해 await() 함수를 제공합니다.
async { } 코루틴 빌더에 대한 자세한 내용은 관련 Deep Dive 에서 보다 자세히 다루었습니다.
<결과>
The answer is 42
Completed in 1072ms
이것은 async 를 사용하지 않은 경우에 비해서 2배 빠른데 그 이유는 두 코루틴을 동시에 수행했기 때문입니다. 코루틴 프레임워크에서는 순차 실행이 기본이기 때문에 동시 수행은 항상 명시적 이어야 합니다.
async 의 지연 실행 (Lazily started async)
async 는 start 파라미터에 CoroutineStart.LAZY 값을 전달하여 실행을 지연시킬 수 있습니다. 이 옵션이 적용된 async 코루틴은 결과 값이 필요한 시점에 await() 이나 start() 함수가 호출되는 시점에 시작됩니다.
위 예제를 보면 두 개의 코루틴을 선언 하면서 실행 옵션은 CoroutineStart.LAZY 를 전달하여 실행 시점을 값이 필요한 시점으로 변경하고 있습니다. 개발자는 원하는 시점에 start() 함수를 호출하여 해당 코루틴을 실행 시킬 수 있습니다. 위 예제에서는 두개의 코루틴을 시작시키고 그 결과를 출력하기 위해서 출력문에서 대기 하게됩니다.
만약 one.start(), two.start() 두 라인을 주석처리하여 시작시키지 않는다면 어떻게될까요? 출력문의 one.await() 이 해당 코루틴을 시작시키고 그 결과를 받고 나서 다음 two.await() 이 호출되기 때문에 결국 순차적으로 수행했을때와 같은 2초정도의 시간이 걸리게됩니다.
비동기 스타일 함수 (Async-style functions)
우리는 앞서 작성한 doSomethingUsefulOne() 과 doSomethingUsefulTwo() 함수들을 호출하는 비동기 스타일의 함수를 정의할 수 있습니다(async wrapper). 이것은 명시적으로 GlobalScope 의 async { }코루틴 빌더를 통해 구현 가능합니다. 그리고 이 함수들이 비동기 연산을 시작시킬 것이고 그 결과를 얻기 위해서는 지연 값(Deferred<T>)을 사용해야 함을 명시적으로 나타내기 위해서 관례적으로 함수명 뒤에 Async 를 붙입니다.
이러한 xxxAsync 함수들은 중단함수들이 아닙니다. 그러므로 어디서든 호출 될 수 있습니다. 하지만 이 함수들의 사용은 항상 코드를 실행함으로써 비동기로 실행(동시 실행)됨을 내포하고 있습니다.
다음 예제는 코루틴이 아닌 곳에서 이 함수들의 사용을 보여주고 있습니다.
위와 같은 비동기 스타일 함수의 사용을 예제로 제공한 이유는 단지 다른 프로그래밍 언어들에서 보편적으로 사용되는 스타일이기 때문입니다. 이러한 방식은 코루틴을 이용한 비동기 프로그래밍에서는 권장되지 않습니다. 그 이유는 에러 처리와 관련이 있는데 만약 xxxAsync 함수 호출과 await() 사이에 오류가 발생한다면 xxxAsync 함수는 오류와 관계없이 계속 수행되는 문제가 있을 것이기 때문입니다.
async 를 이용한 구조화된 동시성(Structured concurrency with async)
앞서 예로 든 코드에서 동시에 수행되는 함수인 doSometingUsefulOne() 과 doSomethingUsefulTwo() 를 이용하여 그 합을 결과로 반환하는 예제를 살펴봅시다. async { } 코루틴 빌더는 CoroutineScope 의 확장 함수로 정의되어 있기 때문에 우리는 이 빌더를 코루틴 스코프 안에서 호출할 수 있고 이것은 coroutineScope() 함수가 제공합니다.
위 예제에서와 같이 구조화된 동시성 모델을 유지하면 일관된 에러 처리를 할 수 있으며 에러의 전파도 정확하게 동작합니다.
취소(Cancellation) 역시 코루틴 계층을 따라 전파됩니다.
<결과>
Second child throw an exception.
First child was canceled.
Computation failed with java.lang.ArithmeticException: Exception on purpose.
결과를 보면 알 수 있듯이, two { }비동기 코루틴이 실패함에 따라서 긴 작업을 수행하고 있던 one { } 비동기 코루틴이 취소가 되고, 이를 호출한 메인 함수로 예외가 전파 됩니다.