2024. 2. 27. 15:09ㆍIT/Android
<정리>
코루틴(Coroutine) - 여러 작업을 동시에 실행할 수 있는 기술.
동기 코드(Synchronous code) - 순차적으로 실행하여 현재 코드가 완료된 경우에만 다음 코드를 호출할 수 있는 구조.
비동기 코드(Asynchronous code) - 현재 코드가 완료되지 않았음에도 불구하고 다음 코드를 호출하여 동시에 실행할 수 있는 구조.
정지 함수(Suspending function) - 정지되었다가 나중에 중단된 이후부터 다시 시작할 수 있는 함수로, 코루틴이나 다른 정지 함수 내에서만 호출이 가능하다.
suspend 수정자 - 일반 함수 선언의 fun 키워드 앞에 붙이면 해당 함수는 정지 함수로 취급된다.
launch() - 새로운 코루틴을 생성하여 해당 코드를 비동기적으로 실행한다. 즉, 람다에서 실행되는 코드들의 반환을 기다리지 않는다. 반환되는 Job 타입의 인스턴스를 활용하면 해당 코루틴의 취소, 상태 확인, 대기, 예외 처리, 관계 설정과 같은 작업들을 수행할 수 있다. 참고로 Job 클래스는 Job 인터페이스를 구현하여 정의한 것으로, 클래스가 코루틴의 실행을 추적하기 위해 정의되었다면 인터페이스는 코루틴의 작업을 정의한다.
import kotlinx.coroutines.*
fun main() {
runBlocking {
println("Weather forecast")
launch {
printForecast() //launch는 printForecast()의 반환을 기다리지 않고 다음 코드를 호출한다.
}
launch {
printTemperature()
}
println("Hi")
}
}
suspend fun printForecast() { //delay()정지 함수의 사용을 위해 suspend 키워드를 사용하여 정지 함수로 전환.
delay(1000)
println("Sunny")
}
suspend fun printTemperature() {
delay(1000)
println("30\u00b0C")
}
/*
Weather forecast
Hi
Sunny
30'C
*/
async() - 새로운 코루틴을 생성하여 해당 코드를 비동기적으로 실행하는 것은 launch()와 다를 바 없지만, 람다에서 실행되는 코드들이 모두 완료되어 반환값이 보장되어야 하는 경우에 사용된다. 반환값은 Job 인터페이스를 구현한 Deferred 클래스 인스턴스에 저장되고, await()의 메서드를 통해 접근이 가능하다.
import kotlinx.coroutines.*
fun main() {
runBlocking {
println("Weather forecast")
val forecast: Deferred<String> = async {
getForecast()
}
val temperature: Deferred<String> = async {
getTemperature()
}
//위의 코루틴이 완료되어 Deferred 인스턴스를 반환할 때까지 해당 코드의 실행은 블록된다.
println("${forecast.await()} ${temperature.await()}")
println("Have a good day!")
}
}
suspend fun getForecast(): String {
delay(1000)
return "Sunny"
}
suspend fun getTemperature(): String {
delay(1000)
return "30\u00b0C"
}
/*
Weather forecast
Sunny 30'C
Have a good day!
*/
병렬 분해(Parallel Decomposition) - 하나의 작업을 여러 개의 하위 코루틴으로 세분화하여 실행하는 방법으로, 모든 하위 코루틴들이 완료되었을 때 해당 결과들을 모아 하나의 최종 결과를 반환한다. 비동기적으로 구현되지만 모든 코루틴이 완료될 때까지 반환하지 않으므로 호출자는 동기적으로 보인다.
import kotlinx.coroutines.*
fun main() {
runBlocking {
println("Weather forecast")
println(getWeatherReport())
println("Have a good day!")
}
}
suspend fun getWeatherReport() = coroutineScope {
val forecast = async { getForecast() }
val temperature = async { getTemperature() }
"${forecast.await()} ${temperature.await()}"
}
suspend fun getForecast(): String {
delay(1000)
return "Sunny"
}
suspend fun getTemperature(): String {
delay(1000)
return "30\u00b0C"
}
/*
Weather forecast
Sunny 30'C
Have a good day!
*/
*coroutineScope : 람다 내부에서 실행되는 모든 코루틴이 완료된 후 반환한다.
코루틴 예외(Exceptions) - launch()로 생성된 코루틴 예외는 즉시 발생하지만 async()로 생성된 코루틴 예외는 Deferred 인스턴스에 저장되므로 코루틴 내부에서 예외를 처리하는 방법과 상위 코루틴으로 예외를 전달하여 처리하는 방법 두 가지를 고려해 볼 수 있다.
1. 코루틴 내부에서 처리.
import kotlinx.coroutines.*
fun main() {
runBlocking {
println("Weather forecast")
println(getWeatherReport())
println("Have a good day!")
}
}
suspend fun getWeatherReport() = coroutineScope {
val forecast = async { getForecast() }
val temperature = async {
try {
getTemperature() //예외 발생.
} catch (e: AssertionError) { //예외 처리.
println("Caught exception $e")
"{ No temperature found }"
}
}
"${forecast.await()} ${temperature.await()}"
}
suspend fun getForecast(): String {
delay(1000)
return "Sunny"
}
suspend fun getTemperature(): String {
delay(500)
throw AssertionError("Temperature is invalid")
return "30\u00b0C"
}
/*
Weather forecast
Caught exception java.lang.AssertionError: Temperature is invalid
Sunny { No temperature found }
Have a good day!
*/
*try-catch문 내부에 async()를 포함시키지 않도록 주의. < async()는 Deferred 인스턴스를 반환하므로 포착되지 않음. >
2. 상위 코루틴으로 전달하여 처리.
import kotlinx.coroutines.*
fun main() {
runBlocking {
println("Weather forecast")
try {
println(getWeatherReport())
} catch (e: AssertionError) { //예외 처리.
println("Caught exception in runBlocking(): $e")
println("Report unavailable at this time")
}
println("Have a good day!")
}
}
suspend fun getWeatherReport() = coroutineScope {
val forecast = async { getForecast() }
val temperature = async { getTemperature() }
"${forecast.await()} ${temperature.await()}" //예외 발생 -> 상위 코루틴으로 전달.
}
suspend fun getForecast(): String {
delay(1000)
return "Sunny"
}
suspend fun getTemperature(): String {
delay(500)
throw AssertionError("Temperature is invalid")
return "30\u00b0C"
}
/*
Weather forecast
Caught exception in runBlocking(): java.lang.AssertionError: Temperature is invalid
Report unavailable at this time
Have a good day!
*/
*코루틴의 예외를 포착하기 위해 Exception을 사용하지 않도록 주의. < 수정이 필요한 예상치 못한 예외를 포착할 수도 있다. >
코루틴 취소(Cancellation) - 취소된 코루틴은 동일한 범위의 다른 코루틴과 상위 코루틴을 취소 시키지 않는다.
import kotlinx.coroutines.*
fun main() {
runBlocking {
println("Weather forecast")
println(getWeatherReport())
println("Have a good day!")
}
}
suspend fun getWeatherReport() = coroutineScope {
val forecast = async { getForecast() }
val temperature = async { getTemperature() }
delay(200)
temperature.cancel() //코루틴 취소.
"${forecast.await()}"
}
suspend fun getForecast(): String {
delay(1000)
return "Sunny"
}
suspend fun getTemperature(): String {
delay(1000)
return "30\u00b0C"
}
/*
Weather forecast
Sunny
Have a good day!
*/
CoroutineScope - 코루틴이 방치되어 리소스를 낭비하지 않도록 범위를 지정하는 인터페이스로, launch()와 async()는 해당 인터페이스의 확장 함수다. Activity(lifecycleScope) 및 ViewModel(viewModelScope)처럼 수명 주기가 잘 정의된 곳에서 지원하는데, 선언된 항목과 동일한 수명 주기를 갖는다. 따라서 Activity가 소멸하면 해당 위치에 선언된 범위도 같이 소멸하게 된다.
CoroutineContext - CoroutineScope 인터페이스의 변수이며 코루틴이 실행될 컨텍스트에 관한 정보를 맵에 고유한 키와 함께 저장하여 제공한다.
CoroutineContext에 포함될 수 있는 항목의 예시
이름 : 코루틴을 식별하기 위한 이름. <기본값 coroutine>
작업 : 코루틴의 수명 주기를 제어.
디스패처 : 작업을 적합한 스레드로 전달. <기본값 Dispatchers.Default>
예외 핸들러 : 코루틴 내부에서 발생하는 예외를 처리.
CoroutineContext는 속성들을 '+' 연산자로 연결하여 생성할 수 있고, 하위 코루틴은 상위 코루틴의 CoroutineContext를 상속받지만 하위 코루틴을 생성하면서 상속받은 속성을 유지하지 않고 변경할 수 있다.
import kotlinx.coroutines.*
fun main() {
// CoroutineContext를 생성합니다.
val coroutineContext = Job() + Dispatchers.Unconfined + CoroutineExceptionHandler { _, exception ->
println("Caught exception: $exception")
}
// Coroutine을 실행합니다.
val coroutineScope = CoroutineScope(coroutineContext)
// Dispatchers.Default로 변경.
coroutineScope.launch(Dispatchers.Default) {
println("Hi")
}
}
디스패처(Dispatcher) - 코루틴이 실행될 스레드를 결정하는 요소.
스레드(Thread) - 프로그램이 실행되는 작업 공간으로 앱이 처음 실행되면 기본 스레드가 생성된다. 기본 스레드는 UI를 담당하므로 비용이 많이 드는 작업은 기본 스레드 외부에서 해결하는 게 좋다. 스레딩 동작에는 차단과 비차단이 있는데, 차단은 스레드를 생성하지 않고 작업의 완료를 기다리는 동기 코드를 의미하고 비차단은 스레드를 생성하여 작업의 완료를 기다리지 않는 비동기 코드를 의미한다.
Kotlin 기본 제공 디스패처
Dispatchers.Main : UI 업데이트 및 유저와의 상호작용을 처리하고 빠른 작업을 실행하는 기본 스레드로 지정하는데 사용된다.
Dispatchers.IO : 디스크 또는 네트워크 I/O를 실행하는 작업자 스레드로 지정하는데 사용된다.
Dispatchers.Default : 계산이 많은 작업을 실행하는 작업자 스레드로 지정되는데 사용되고, 컨텍스트에 디스패처가 없는 상태에서 launch(), async()를 호출하면 해당 디스패처가 지정된다.
import kotlinx.coroutines.*
fun main() {
runBlocking {
println("${Thread.currentThread().name} - runBlocking function")
launch {
println("${Thread.currentThread().name} - launch function")
withContext(Dispatchers.Default) { //디스패처를 Main -> Default 변경.
println("${Thread.currentThread().name} - withContext function")
delay(1000)
println("10 results found.")
}
println("${Thread.currentThread().name} - end of launch function")
}
println("Loading...")
}
}
/*
main @coroutine#1 - runBlocking function
Loading...
main @coroutine#2 - launch function
DefaultDispatcher-worker-1 @coroutine#2 - withContext function
10 results found.
main @coroutine#2 - end of launch function
*/
코루틴의 작업 상태
시작 : 지정된 경계의 범위내에서 코루틴 실행.
완료 : 범위에서 실행된 모든 하위 코루틴이 작업을 완료하고 해당 코루틴 반환.
취소 : 상위 코루틴이 취소되어 하위 코루틴들도 취소.
실패 : 예외가 발생하여 하위 코루틴들을 취소시키고 상위 코루틴으로 예외를 전달.
'IT > Android' 카테고리의 다른 글
Jetpack Compose - Something to do with REST (2) | 2024.03.05 |
---|---|
Jetpack Compose - Coroutine, Unit Test (1) | 2024.02.27 |
Jetpack Compose - Adaptive App Practice.ver2 (0) | 2024.02.22 |
Jetpack Compose - Adaptive App Practice (0) | 2024.02.14 |
Jetpack Compose - Adaptive app with dynamic navigation (2) | 2024.02.14 |