목표
launch
와async
빌더 사용법과 각각의 특징 설명Deferred
객체 사용법과 비동기 결과 처리 방법- 실제 네트워크 호출을 예로 들어 비동기 처리 실습
코루틴 빌더 함수
코루틴을 생성하는 데 사용하는 함수를 코루틴 빌더 함수라고 한다.
코루틴 빌더 함수 종류
- runBlocking
- launch
- async
launch
launch
는 코루틴을 추상화한 Job
객체를 반환하며, 이 Job
객체가 cancel
되면 코루틴도 cancel
됩니다.
launch
의 coroutineContext
는 CoroutineScope
를 통해 상속받고, 추가적인 context
요소를 넣을 수도 있습니다.
- 만약, 동일한 key를 가진 context 요소가 이미 있었다면 해당 값을 덮어씌우는 형태로 동작합니다.
위 이미지를 보면 확인할 수 있듯이, launch
에 context
를 따로 설정하지 않으면 EmptyCoroutineContext
로 설정되어 부모의 context
를 그대로 상속합니다. 반면에, launch
에 context
를 설정하면, 부모의 context
를 상속받되, 중복된 key가 있다면 덮어씌우는 형식으로 동작합니다.
코루틴 상태
코루틴은 위와 같은 상태를 가지며, 상태값은 Job 객체를 통해 외부에 노출이 됩니다.
- 생성(New): 코루틴 빌더를 통해 코루틴을 생성할 때의 상태이며, 자동으로 실행 중 상태로 넘어감(Lazy 설정 시, 생성 상태로 대기)
- 실행 중(Active): 코루틴이 실제 실행 중일때와 실행 중 일시 중단되었을 때 모두 실행 중 상태 유지
- 실행 완료(Completed): 코루틴이 완료된 상태
- 취소 중(Cancelling): cancel() 함수 호출을 통해 취소가 진행 중인 상태로, 아직 취소가 완료된 상태가 아니라 코루틴은 여전히 실행중
- 취소 완료(Cancelled): 코루틴의 취소 확인 시점에 취소가 확인되면, 취소 완료 상태가 되며 코루틴은 더 이상 실행되지 않음
Job 객체는 코루틴을 추상화한 객체이기 때문에, 필드 값을 외부에서 볼 수는 없고, 코루틴의 상태를 아래 필드를 통해 간접적으로 노출합니다.
isActive
: 코루틴 활성화 여부- 활성화: 코루틴이 실행된 후 취소 요청되거나 실행이 완료되지 않은 상태
isCancelled
: 코루틴이 취소 요청되었는지 여부- 취소 요청이 되었다고 해서 바로 코루틴이 취소되는 것은 아니며, isCancelled가 true가 되면, 실제로는 코루틴이 아직 실행중이어도 코루틴이 활성화된 상태로 보지 않아, isActive가 false가 됨
isCompleted
: 코루틴 실행 완료 여부
job으로 할 수 있는 일
join()
과joinAll()
을 통한 작업 순차처리
특정launch
코루틴이 완료된 후 실행되어야 하는 코루틴이 있다면, 두 코루틴 사이에join()
함수를 활용해 순차처리하도록 설정할 수 있습니다.
참고!
위 예시처럼, join()은join()
을 호출한 코루틴을 제외하고, 이미 실행중인 다른 코루틴을 일시 중단하진 않습니다.
복수의 코루틴의 실행이 모두 끝날 때까지 호출부의 코루틴을 일시 중단시켜야할 때는 joinAll()을 활용할 수 있습니다.
사용 예) 여러 API를 비동기적으로 호출하여 결과를 모두 받아온 후 병합하는 코루틴을 수행시켜야 할 때 사용
joinAll()
함수를 테스트할 때에는 Dispatcher
설정에 따라 사용할 수 있는 스레드 개수가 달라 혹 작업 속도에 영향을 주지 않을지 궁금하여 launch
에 Dispatcher
를 설정한 경우와 아닌 케이스를 나누어 테스트를 진행했습니다. Dispatcher를 따로 설정할 경우, runBlocking 내부에서 돌아도 main 스레드가 아닌 다른 스레드에서 동작하여 Dispatcher
를 설정하지 않을 때보다 속도가 빠를 것을 기대했으나 큰 차이는 없었습니다.
- 코루틴 지연 시작
기본적으로 launch 함수를 사용한 코루틴은 생성한 이후 사용할 수 있는 스레드가 있는 경우 곧바로 실행됩니다. 만약, 코루틴은 지금 생성하되, 이후에 실행해야 한다면 코루틴을 지연 시작(Lazy Start)할 수 있는 기능을 이용할 수 있습니다.
위 예시처럼, lazy
로 설정한 launch
코루틴은 해당 job
의 start()
함수를 명시적으로 호출해야 실제로 실행이 됩니다. 참고로, 위 예시에서 immediateJob
의 active
값이 true
로 나온 이유는, immediateJob
을 수행한 스레드와 job
의 상태를 출력하는 스레드가 서로 상이하여서 immediateJob
이 완전히 종료되기 전에 active
값을 확인했기 때문입니다.
- 코루틴 취소
간혹, 작업을 하다보면 사용자가 요청을 취소할 경우도 있습니다. 이미 취소된 요청인데, 관련된 코루틴 작업을 진행하면 스레드를 소비하게 되므로 애플리케이션 성능에 좋지 않습니다. 이 경우, job의 cancel()함수를 통해 코루틴을 취소할 수 있습니다.
사용 예) 특정 시간동안 끝나지 않은 코루틴은 취소하고자 할 경우 사용
아래 예시에서 확인할 수 있듯이, 1000ms을 10번 반복하는 launch
코루틴을 만들었을 때, 호출부에서 3500ms을 기다린 후 수행이 완료되지 않은 부분부터 cancel
하여 로그가 10번이 아닌, 4번 찍힘을 확인할 수 있습니다.
cancel()
함수를 사용할 때에는 순차 처리 작업에 유의해야 합니다. 예를 들어, A라는 코루틴 취소가 완료된 이후 B라는 코루틴이 시작되어야 한다면, cancelAndJoin()
함수를 사용해야 합니다. (cancel()
호출 후 바로 join()
호출해도 무방)
cancel()
호출 시, 코루틴은 즉시 취소되는 것이 아닌, Job 객체 내부의 취소 확인용 플래그를 '취소 요청됨'으로 변경함으로써 코루틴이 취소되어야 함을 알립니다. 이후 미래 어느 시점에 코루틴의 취소 요청이 있는지 확인 후 실제 취소 작업이 이루어집니다.- 코루틴의 취소 요청 확인 시점은, 일반적으로 일시 중단 시점이나 코루틴이 실행을 대기하는 시점으로, 이 시점이 없다면 코루틴은 취소되지 않습니다.
cancelAndJoin()
함수는 취소 작업이 완료될 때까지 해당 코루틴의 호출부의 코루틴이 일시 중단됩니다.
[ 일시 중단 시점이 없는 코루틴 취소하는 법 ]
예를 들어, 아래에서 cancel
하고자 하는 job
에는 일시 중단 시점(예: 코루틴 내 delay
함수 호출)이나 코루틴이 실행을 대기하는 시점이 없어서, cancel
되지 않습니다.
이처럼, 중단 가능 지점이 없어 cancel
되지 않는 코루틴이 있다면 아래 방법을 통해 취소 여부 확인을 할 수 있습니다.
- 코루틴 내부에
delay
를 통한 취소 확인 - 코루틴 내부에
yield
를 넣어 취소 확인 CoroutineScope.isActive
를 사용한 취소 확인
위 테스트와 달리 cancel()
하고자 하는 job
내에 중단 가능 지점이 있어 cancel()
이 되는 것을 아래 예시에서 확인할 수 있습니다.
하지만 아래 예시는 수행하는 코루틴 내에 강제로 중단 가능 지점(delay
, yield
)을 넣는 방식이라, 코루틴이 잠시 멈추거나, 스레드 사용을 양보하여 성능을 저하시키게 됩니다.
이럴 때 사용할 수 있는 방법이 CoroutineScope.isActive
를 사용한 취소 확인 방법입니다. CoroutineScope
는 코루틴의 활성화 여부를 나타내는 isActive
필드를 가지고 있으며, 코루틴에 취소 요청이 들어갔을 때 isActive
를 false
로 바꾸어줍니다. 따라서, 아래 예시처럼 isActive
필드값을 중단 지점 없는 코루틴에서 성능 저하 없이 코루틴을 취소시켜줍니다.
- 취소 요청이 되면,
Job.isCancelled
가true
가 되고Job.isActive
가false
가 되기 때문에CoroutineScope
의isActive
를 활용할 수 있는 것입니다.
참고) 만약 위 예시에서 launch
의 context를 따로 설정하지 않으면, this.isActive
로 취소 체크가 불가능합니다. 그 이유는, runBlocking
의 context를 상속 받은 launch
는 메인스레드에서 계속 동작하기 때문에, job.cancel()
구문을 메인 스레드가 접근할 수 없기 때문입니다.
async
앞서 살펴본 launch
코루틴 빌더는 기본적으로 수행 결과를 반환하지 않습니다. 코루틴 라이브러리는 비동기 작업으로부터 결과를 수신해야 하는 경우 사용할 수 있는 async 코루틴 빌더를 제공합니다. async
는 launch
는 Job
을 반환했던 것과 달리, Deferred
를 반환하여 Deferred
객체를 통해 코루틴 결과값을 얻습니다.
async
코루틴 빌더는 launch
코루틴 빌더와 마찬가지로 CoroutineScope
의 확장함수이며, 기본적인 호출 방법은 launch
와 유사합니다. start
필드를 통해 LAZY 설정도 할 수 있고, 특정 context에서 수행될 수 있도록 Dispatcher도 설정이 가능합니다.
async가 반환하는 Deferred 객체는 Job을 상속하고 있어, 기본적으로 Job과 같은 코루틴 추상화 객체입니다. Deferred는 Job보다 좀 더 편리한 메소드를 제공하며 미래 어느 시점에 코루틴 결과값을 가져올 수 있는 역할도 함께 제공합니다.
Deferred 객체를 통해 결과값 수신을 기다리기 위한 await() 함수가 존재합니다. await() 함수를 호출하면 응답을 가져올 때까지 호출부의 코루틴을 일시 중단(Job의 join()과 유사한 역할)하게 됩니다. join()에 joinAll()이 있었던 것처럼, 여러 Deferred 객체 결과값 반환을 기다리는 awaitAll() 함수도 제공합니다.
awaitAll()
함수 관련한 개인적인 아쉬움은, 결과를 List 형태로 제공준다는 점입니다. 여러 개의async
결과물을 map 형태로 넣어줬다면 좀 더 사용하기 좋을 것 같다는 생각이 듭니다.- 예를 들어, 특정 클래스에 데이터를 채우기 위해 여러 API 호출이 필요하다면
async
를 활용할 수 있습니다. 이때,awaitAll()
로 한 번에 결과를 들고오고 싶지만 각 필드에 어떤Deferred
결과를 넣을지List
로 전달받으면 코드에 명시적으로 표현하기가 어렵습니다.
- 예를 들어, 특정 클래스에 데이터를 채우기 위해 여러 API 호출이 필요하다면
withContext
withContext는 전달받은 context 내에서 람다식으로 전달받은 내용을 수행하고 결과를 반환합니다. withContext
는 async
와 await()
함수를 각각 호출을 해야 코루틴 결과를 반환받았던 것보다 좀 더 간결하게 표현할 수 있습니다.
withContext
와 async
-await
동작 방식은 내부적으로는 차이가 있습니다.
withContext
: 코루틴 빌더가 아니기 때문에, 새로운 코루틴을 생성하는게 아닌 기존 코루틴을 유지합니다. 단지,withContext
에 설정된 context로 코루틴 수행처를 옮겨갈 뿐(컨텍스트 스위칭)입니다. 따라서, 하나의 코루틴 내에서 실행되는 것이기 때문에 동기적으로 실행됩니다.async-await
: 코루틴 빌더이기 때문에, 호출처와는 다른 새로운 코루틴을 생성합니다. 따라서, 호출처의 코루틴과 별개의 코루틴이기 때문에 호출처의 코루틴 수행 도중에 async 결과를 받아 동기적으로 실행하고자await
을 호출하는 것입니다.
다만, withContext
를 사용할 때에는 몇 가지 주의가 필요합니다.
withContext
는 새로운 코루틴을 생성하는게 아니기 때문에, 병렬적으로 수행할 수 있는 작업을withContext
로 각각 나타낼 경우 동기적 수행을 통해 수행 시간이 배로 걸리게 됩니다.- withContext는 호출처의 코루틴을 컨텍스트 스위칭을 통해 다른 스레드에서 수행하는 것이기 때문에, 코루틴이 동기적으로 수행되어 병렬 수행 가능한 withContext를 여러개 만들더라도 동기적으로 동작합니다.
runBlocking과 launch 차이
runBlocking 함수와 launch 함수는 모두 코루틴 빌더 함수이지만, 코루틴 빌더를 호출한 스레드를 사용하는 방법에 차이가 있습니다.
runBlocking 함수의 동작 방식
- runBlocking 함수가 호출되면, runBlocking 코루틴 실행이 완료될때까지 호출부의 스레드를 차단(block)하고 사용합니다.
- runBlocking 함수가 호출된 스레드가 아닌 다른 스레드에서 실행되더라도, 해당 코루틴이 실행되는 동안 runBlocking 함수를 호출한 스레드는 차단됩니다. runBlocking 코루틴이 완료되어야 차단이 풀립니다.
- 이때, 호출된 스레드는 배타적으로 사용되기 때문에, runBlocking 하위 코루틴은 호출부 스레드를 사용할 수 있습니다.
runBlocking 함수의 차단은 스레드 블로킹과는 다릅니다.
스레드 블로킹은 해당 스레드가 어떤 작업에도 사용될 수 없게 차단된다면,
runBlocking 함수의 차단은 runBlocking 코루틴과 자식 코루틴들을 제외한 다른 작업이 스레드를 사용할 수 없는 것입니다.
launch 함수의 동작 방식
launch 함수는 runBlocking과 달리, 호출부의 스레드를 차단하지 않습니다. 따라서, launch 코루틴 내 delay 같은 작업으로 인해 현재 스레드를 사용하지 않는다면 다른 작업에 해당 스레드를 양보합니다.
참고 자료
'PROGRAMMING LANGUAGE > KOTLIN' 카테고리의 다른 글
[Effective Kotlin] 1장 안정성 (0) | 2024.06.23 |
---|---|
[Kotlin Coroutine] 코루틴 컨텍스트와 디스패처의 이해 (0) | 2024.06.19 |
[Kotlin Coroutine] 코루틴 기본 이해 & JVM에서의 async (0) | 2024.06.08 |
[Kotlin] Collections의 Iterable과 Sequences 차이점 (0) | 2023.08.23 |
[Kotlin]enum class와 data class (0) | 2022.08.25 |