brunch

You can make anything
by writing

C.S.Lewis

by 서준수 Sep 21. 2023

코루틴 async

코틀린 코루틴 (8)

루틴 async


코루틴 빌더에서 async의 존재에 대해서 이야기한 바 있다. 지금까지는 launch 위주의 예제만 살펴보았다. 그리고 launch는 Job 객체를 반환한다는 것까지 알게 되었다.


이번에는 코루틴 빌더 중 async에 대해서 살펴본다. async는 비동기 작업을 실행하고 해당 작업의 결과를 Deferred 객체로 반환하는 함수이다.


(객체라고 표현했으나 실제로 Job과 Deferred는 인터페이스이다. 여기서 객체라고 표현한 것은 Job이나 Deferred를 구현한 클래스의 인스턴스로 생각하면 된다. 이 말이 무슨 소리인지 모르겠다고? 그렇다고 async를 이해하는데 지장을 주지 않는다. 그건 필요하면 별도로 학습하면 된다. 지금 갑자기 객체니 클래스니 하는 것에 대한 것을 공부하는 것은 삼천포로 빠지는 것이다.)


비동기 작업? 비동기라는 말은 delay 함수에서 언급되어 살펴보았고 suspend 함수에서 좀 더 자세히 다루어 보았다. 그럼에도 async에서 비동기를 논한다는 것은 또 다른 방식으로 활용할 수 있는 형태이기 때문일 것이라는 추측이 가능하다.


정리하자면 launch와 비교했을 때 비동기 방식과 반환하는 객체가 다르다는 것이다. 따라서 의문을 가져야 할 부분은 크게 두 가지로 정리할 수 있다.

1. async의 비동기는 어떤 형태인가?

2. Deferred는 무엇인가?


먼저 반환하는 Deferred에 대해서 알아보자. async는 Job을 반환하지 않는다. 대신에 Deferred 객체를 반환한다. 그래서 Deferred는 무엇이냐? Deferred는 결괏값을 가지는 Job이다. 그럼 Deferred는 결괏값을 어떻게 가져올까? await()를 사용하면 된다. await()는 async의 작업이 끝나길 기다렸다가 작업이 완료되면 결과를 반환한다.


그러면 또다시 물음표! async의 작업은 어떤 작업일까? async는 코루틴 빌더라고 했다. 코루틴 빌더를 통해 생성된 코루틴은 비동기적으로 동작한다고 했다. 결국 async에서 수행하는 작업은 비동기 작업이다. 그러면 비동기적으로 동작을 하면서 await()가 작업이 완료되면 결과를 반환하는 형태라는 말이 된다. 이것이 앞서 품었던 두 가지 의문을 합한 것이다.


무슨 말인지 와닿지 않을 수 있다. 예제 코드를 보면서 좀 더 생각해 보자.

위 코드에서 measureTimeMillis은 블록 내 작업의 수행 시간을 ms 단위로 반환해 준다. 그러면 위 코드의 수행 시간은 몇 ms일까? suspend 함수인 getFirstValue()는 delay 함수에 의해서 1000ms 정지할 것이다. getSecondValue() 역시 마찬가지다. 그러면 최소 2000ms는 정지를 하게 된다. 그리고 출력하는 작업도 해야 하니 2000ms 이상 소모될 것으로 예상할 수 있다. 개발 환경에 따라 차이가 있겠지만 실제로 대략 2020ms 정도 걸린다. 결과를 놓고 보면 비동기적으로 동작하지 않았다는 것을 알 수 있다.


그림으로 표현하면 다음과 같다.


코루틴이 없으니까 그렇겠지! 그럼 launch로 코루틴을 만들어 보자!

제대로 될까? 일단 비동기 작업을 위해 코루틴을 적용해야겠다는 생각까지 할 수 있다고 치자. 그래서 위와 같은 코드를 작성했다고 하자. 그러면 이제 위 코드를 보고 실행결과를 추측해 보자. getFirstValue()가 호출되면 1000ms 정지되는가? 아니다. 다음 코루틴으로 제어권인 넘어간다. 그래서 곧바로 getSecondValue()가 호출되고 역시나 다음 작업인 출력문으로 넘어간다. 빠른 시간에 measureTimeMillis의 블록이 수행된 것이다. 결국 출력문이 실행될 때 각 함수의 반환은 1000ms 후에 이뤄지기 때문에 firstValue와 secondValue는 여전히 초깃값을 가지고 있다. 따라서 실행결과는 다음과 같다.

,
Completed in 3 ms
First work is done
Second work is done

그림으로 표현하면 다음과 같다.


여기서 문제점은 무엇인가? 비동기적으로 작업이 이뤄지고 있지만 실제로 결괏값 사용하려는 시점과 반환하는 시점이 다르다. 그러면 결괏값을 사용하려고 할 때 반환이 이뤄지면 해결될 것이다. 바로 이때 async와 await의 조합이 빛을 발하는 것이다.

async는 Deferred 객체를 반환한다고 했다. Deferred 객체는 결괏값을 가지는 Job이라고 했다. 그래서 firstValue와 secondValue의 타입을 보면 Deferred<String>로 되어 있는 것을 볼 수 있다. Deferred 객체에 String 타입의 결괏값이 있다는 것을 알 수 있다. 그리고 출력문도 자세히 살펴보면 단순히 firstValue를 출력하는 것이 아니다. fitstValue는 바로 직전에 말했듯이 Deferred<String> 타입이기 때문에 원하는 String 타입이 아니다. 따라서 결괏값을 반환해 주는 await()를 붙인다.


await()는 단순히 결괏값을 반환하는 것이 아니라 작업이 완료될 때까지 기다렸다가 반환한다고 했다. 그렇기 때문에 launch와 다르게 초깃값이 아닌 실제로 작업이 끝나서 반환된 First value라는 문자열을 출력할 수 있게 되는 것이다.


launch와 동일한 면도 있다. 바로 async도 코루틴 빌더라는 것이다. 그렇다면 이 작업도 비동기적으로 수행된다는 것이다. 즉 getFirstValue()를 호출하고 해당 작업이 끝나길 기다린 후 getSecondValue()를 호출해서 해당 작업이 끝나길 기다려서 결괏값을 가져오는 것이 아니다. 이렇게 동작하는 것은 첫 번째 예제의 동작이다. 대신 비동기이기 때문에 getFirstValue()를 호출하고 delay 되면서 제어권이 다음 코루틴에게 넘어가서 getSecondValue()가 곧바로 호출된다. 그 후 각 코루틴의 작업이 완료되었을 때 결괏값을 가져온다.


그러면 이 예제는 수행 시간이 얼마나 될까? 실제 실행결과는 다음과 같다.

First work is done
Second work is done
First value, Second value
Completed in 1012 ms

비동기적으로 수행하여 첫 번째 예제에 비하면 절반 정도밖에 시간을 소모하지 않는 것을 알 수 있다.


그림으로 표현하면 다음과 같다.

요약하자.

- async는 코루틴 빌더이다.
- async는 비동기 작업을 실행하고 해당 작업의 결과를 Deferred 객체로 반환하는 함수이다.
- await()는 async의 작업이 끝나길 기다렸다가 작업이 완료되면 결과를 반환한다.
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari