일시 중단 함수와 코루틴
1. 일시 중단 함수란?
- 주로 코루틴의 비동기 작업과 관련된 복잡한 코드들을 구조화하고 재사용할 수 있는 코드의 집합으로 만드는 데 사용
- `suspend fun` 키워드로 선언되는 함수로 함수 내에 일시 중단 지점을 포함할 수 있는 특별한 기능을 수행함
- 일시 중단 지점"은 코루틴(coroutine) 내에서 실행이 일시적으로 중단될 수 있는 지점을 의미
- ex) delay 함수의 경우 일시 중단 지점을 포함하므로 일반 함수는 delay 함수가 포함시킬 수 없고 오직 suspend fun 함수만 delay 함수를 포함시킬 수 있음

2. 일시 중단 함수는 코루틴이 아니다
- 일시 중단 함수는 코루틴 내부에서 실행되는 코드의 집합일 뿐, 코루틴이 아님
- 일시 중단 함수는 기존의 함수와 똑같은 재사용이 가능한 코드 블록
- 만약 일시 중단 함수를 코루틴처럼 사용하고 싶을 경우 일시 중단 함수를 코루틴 빌더로 감싸야함
3. 일시 중단 함수를 별도의 코루틴 상에서 실행하기
- 앞서 언급했다시피 일시 중단 함수를 새로운 코루틴에서 실행하고 싶을 경우 일시 중단 함수를 코루틴 빌더 함수로 감싸면 됨
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
fun main() = runBlocking<Unit> { | |
val startTime = System.currentTimeMillis() | |
launch { | |
delayAndPrintHelloWorld() | |
} | |
launch { | |
delayAndPrintHelloWorld() | |
} | |
println(getElapsedTime(startTime)) | |
} | |
suspend fun delayAndPrintHelloWorld() { | |
delay(1000L) | |
println("Hello World") | |
} |

부연 설명
- launch 함수가 호출돼 생성된 코루틴들은 실행되자마자 delayAndPrintHelloWorld 함수의 호출로 1초간 쓰레드 사용 권한을 양보함
- 자유로워진 쓰레드는 다른 코루틴인 runBlocking 코루틴에 의해 사용될 수 있으므로 곧바로 마지막 줄의 getElapsedTime이 실행됨
- 따라서 코드의 실행 결과를 보면 지난 시간이 약 0초에 가까운 것을 확인할 수 있으며 이후 1초 정도 지나고 나서 재개된 코루틴들에 의해 `Hello World` 문자열이 두 번 출력됨
일시 중단 함수의 사용
- 일시 중단 함수는 내부에 일시 중단 가능 지점을 포함할 수 있기 때문에 일시 중단 할 수 있는 곳에서만 호출 가능
- 코틀린에서 일시 중단이 가능한 지점은 다음과 같이 두 가지
- 코루틴 내부
- 일시 중단 함수
- 코틀린에서 일시 중단이 가능한 지점은 다음과 같이 두 가지
1. 코루틴 내부에서 일시 중단 함수 호출하기
- 일시 중단 함수는 코루틴의 일시 중단이 가능한 작업을 재사용이 가능한 블록으로 구조화할 수 있도록 만들어진 함수
- 코루틴은 언제든지 일시 중단 함수를 호출할 수 있음
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
fun main() = runBlocking<Unit> { | |
delayAndPrint(keyword = "I'm Parent Coroutine") | |
launch { | |
delayAndPrint(keyword = "I'm Child Coroutine") | |
} | |
} | |
suspend fun delayAndPrint(keyword: String) { | |
delay(1000L) | |
println(keyword) | |
} |

2. 일시 중단 함수에서 다른 일시 중단 함수 호출하기
- 일시 중단 함수는 또 다른 일시 중단 함수에서 호출될 수 있음
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
fun main() = runBlocking<Unit> { | |
val startTime = System.currentTimeMillis() | |
// launch 블록에서 suspend 함수 호출 | |
launch { | |
val keywords = searchByKeyword("Kotlin") | |
for (keyword in keywords) { | |
println(keyword) | |
} | |
} | |
println(getElapsedTime(startTime)) | |
} | |
// suspend 함수로 정의 | |
suspend fun searchByKeyword(keyword: String): Array<String> { | |
val dbResults = searchFromDB(keyword) | |
val serverResults = searchFromServer(keyword) | |
return arrayOf(*dbResults, *serverResults) | |
} | |
suspend fun searchFromDB(keyword: String): Array<String> { | |
delay(1000L) | |
return arrayOf("[DB]${keyword}1", "[DB]${keyword}2") | |
} | |
suspend fun searchFromServer(keyword: String): Array<String> { | |
delay(1000L) | |
return arrayOf("[Server]${keyword}1", "[Server]${keyword}2") | |
} | |
fun getElapsedTime(startTime: Long): String { | |
val elapsedTime = System.currentTimeMillis() - startTime | |
return "Elapsed time: ${elapsedTime}ms" | |
} |

부연 설명
- searchByKeyword 일시 중단 함수는 searchFromDB와 searchFromServer를 호출하며, 결과로 받은 값들을 합쳐 반환
- 위에서 거론된 함수들은 모두 일시 중단 함수들이며 searchFromDB와 searchFromServer 내부의 delay(1000L)로 인해 1초간 일시 중단돼 결괏값을 반환하는 데 1초 정도 걸림
3. 일시 중단 함수에서 코루틴 실행하기
- launch나 async 같은 코루틴 빌더 함수는 CoroutineScope의 확장 함수로 선언되어 있음
- 따라서 일시 중단 함수에서 launch나 async 같은 코루틴 빌더 함수를 호출하기 위해서는 일시 중단 함수 내부에서 CoroutineScope 객체에 접근할 수 있어야 함
- coroutineScope 일시 중단 함수를 사용하면 일시 중단 함수 내부에 새로운 CoroutineScope 객체를 생성할 수 있음
- coroutineScope는 구조화를 깨지 않는 CoroutineScope 객체를 생성하며 생성된 CoroutineScope 객체는 coroutineScope의 block 람다식에서 수신 객체(this)로 접근 가능
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
fun main() = runBlocking<Unit> { | |
val startTime = System.currentTimeMillis() | |
val results = searchByKeywordAsync("Keyword") | |
println("[결과] ${results.toList()}") | |
println(getElapsedTime(startTime)) | |
} | |
suspend fun searchByKeywordAsync(keyword: String): Array<String> = coroutineScope { | |
val dbResultsDeferred = async { | |
searchFromDB(keyword) | |
} | |
val serverResultsDeferred = async { | |
searchFromServer(keyword) | |
} | |
return@coroutineScope arrayOf(*dbResultsDeferred.await(), *serverResultsDeferred.await()) | |
} |

부연 설명
- async 코루틴 빌더를 사용해 searchFromDB를 실행하는 코루틴인 dbResultsDeferred와 searchFromServer를 실행하는 코루틴인 serverResultsDeferred를 생성하고, 결과가 반환되는 부분에서 await를 호출해 searchFromDB와 searchFromServer 작업이 함께 실행될 수 있도록 처리
- 핵심은 searchByKeywordAsync 일시 중단 함수가 호출됐을 때 코루틴이 다음과 같이 구조화된다는 것
- runBlocking 코루틴에서 searchByKeyword 일시 중단 함수를 호출하면 내부에서 coroutineScope 함수를 통해 새로운 Job 객체를 가진 CoroutineScope 객체가 생성됨
- 그 자식으로 데이터베이스와 서버로부터 데이터를 가져오는 코루틴이 각각 생성됨
- 따라서 코드를 실행해 보면 searchFromDB 작업과 searchFromServer 작업이 서로 다른 코루틴에서 실행돼 1초 정도 만에 실행 완료되는 것을 확인 가능

4. supervisorScope 사용해 일시 중단 함수에서 코루틴 실행하기
- 3번 코드에 한 가지 문제가 존재함
- 데이터베이스에서 데이터를 조회하는 코루틴이 오류를 발생시키면 부모 코루틴으로 오류를 전파해 서버에서 데이터를 조회하는 코루틴까지 취소될 수 있음
- 심지어 일시 중단 함수를 호출한 코루틴에까지 예외가 전파돼 호출부의 코루틴까지 모두 취소됨
- supervisorScope 일시 중단 함수를 통해 위 문제를 해결할 수 있음
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
fun main() = runBlocking<Unit> { | |
val startTime = System.currentTimeMillis() | |
val results = searchByKeywordAsyncSupervisorScope("Keyword") | |
println("[결과] ${results.toList()}") | |
println(getElapsedTime(startTime)) | |
} | |
suspend fun searchByKeywordAsyncSupervisorScope(keyword: String): Array<String> = supervisorScope { | |
val dbResultsDeferred = async { | |
throw Exception("dbResultsDeferred에서 예외가 발생했습니다") | |
searchFromDB(keyword) | |
} | |
val serverResultsDeferred = async { | |
searchFromServer(keyword) | |
} | |
val dbResults = try { | |
dbResultsDeferred.await() | |
} catch (e: Exception) { | |
arrayOf() // 예외 발생 시 빈 결과 반환 | |
} | |
val serverResults = try { | |
serverResultsDeferred.await() | |
} catch (e: Exception) { | |
arrayOf() | |
} | |
return@supervisorScope arrayOf(*dbResults, *serverResults) | |
} |

부연 설명
- dbResultsDeferred 코루틴에서 예외가 발생해 해당 코루틴이 취소되더라도 부모로 supervisorScope를 통해 생성되는 SupervisorJob 객체를 가지므로 예외가 부모 코루틴으로 전파되지 않는 것을 확인 가능
참고
코틀린 코루틴의 정석 (조세영 저)
반응형
'Kotlin > 코틀린 코루틴의 정석' 카테고리의 다른 글
[Kotlin 코루틴] 코루틴 심화 (1) | 2024.11.29 |
---|---|
[Kotlin 코루틴] 코루틴의 이해 (0) | 2024.11.26 |
[Kotlin 코루틴] 예외 처리 (0) | 2024.11.22 |
[Kotlin 코루틴] 구조화된 동시성 (0) | 2024.11.21 |
[Kotlin 코루틴] CoroutineContext 정리 (0) | 2024.11.19 |