1. 코루틴 취소의 기본 개념
- 코틀린 코루틴의 취소(Cancellation)는 실행 중인 비동기 작업을 안전하게 중단하는 메커니즘입니다.
- 사용자가 작업을 취소하거나, 타임아웃이 발생하거나, 오류가 발생할 때 코루틴을 취소해야 하는 상황이 자주 발생합니다.
- 코루틴 취소는 코루틴 라이브러리의 핵심 기능으로, 자원 낭비를 방지하고 앱의 응답성을 유지하는 데 중요합니다.
1.1 취소가 필요한 이유
- 실행 중인 작업이 더 이상 필요하지 않을 때 시스템 자원을 절약합니다.
- 사용자 경험을 향상시킵니다 - 사용자가 요청한 작업을 즉시 중단할 수 있습니다.
- 메모리 누수를 방지합니다 - 더 이상 필요하지 않은 작업이 계속 실행되면 메모리 누수가 발생할 수 있습니다.
- 오류 처리를 단순화합니다 - 작업 중 오류가 발생했을 때 관련된 모든 작업을 깔끔하게 정리할 수 있습니다.
1.2 코루틴 취소의 특성
- 협력적(Cooperative): 코루틴 취소는 강제적이지 않고 협력적입니다. 코루틴은 취소 신호를 확인하고 스스로 종료해야 합니다.
- 구조적(Structured): 부모-자식 관계에 따라 취소가 전파됩니다. 부모가 취소되면 모든 자식도 취소됩니다.
- 예외 기반(Exception-based): 취소는 내부적으로
CancellationException을 사용하여 구현됩니다. - 선언적(Declarative): 복잡한 콜백 없이 간결하게 취소 로직을 작성할 수 있습니다.
2. 코루틴 취소의 작동 방식
- 코루틴 취소는 복잡한 내부 메커니즘을 가지고 있지만, 사용자 관점에서는 비교적 간단합니다.
- 취소의 핵심 개념과 내부 작동 방식을 이해하면 효과적으로 활용 할 수 있습니다.
2.1 취소의 기본 흐름
Job.cancel()또는 관련 메서드가 호출됩니다.- Job의 상태가 '취소 중(Cancelling)'으로 변경됩니다.
CancellationException이 코루틴 내부로 전파됩니다.- 코루틴이 다음 일시 중단 지점(suspension point)에 도달하면 예외가 발생합니다.
- 코루틴이 예외를 처리하고 정리 작업을 수행합니다.
- Job의 상태가 '취소됨(Cancelled)'으로 변경됩니다.
Job 상태 변화
취소 시 Job 상태 전이: Active → Cancelling → Cancelled
2.2 취소와 CancellationException
- 코루틴 취소는 내부적으로
CancellationException을 사용하여 구현됩니다. - 이는 일반 예외와 달리 코루틴의 정상적인 종료로 간주되어 부모 코루틴에게 전파되지 않습니다.
- 개발자는 이 예외를 직접 처리하거나 무시할 수 있습니다.
fun main() = runBlocking {
val job = launch {
try {
repeat(1000) { i ->
println("코루틴 실행 중... $i")
delay(500)
}
} catch (e: CancellationException) {
println("코루틴이 취소되었습니다: ${e.message}")
throw e // 재전파하는 것이 일반적인 관행입니다
}
}
delay(1300) // 일부 반복 실행 후
println("취소 요청")
job.cancel("사용자에 의한 취소") // 취소 이유 지정
job.join() // 취소 완료 대기
println("메인 코루틴 종료")
}
이 예제에서는 코루틴을 시작하고 일정 시간 후에 취소합니다. 코루틴은 CancellationException을 포착하여 메시지를 출력하고 다시 던집니다.
2.3 취소의 협력적 특성
- 코루틴 취소는 협력적입니다. 즉, 코루틴 코드가 취소에 협력해야 합니다.
- 코루틴은 다음 방법 중 하나로 취소에 협력할 수 있습니다:
- 일시 중단 함수(
delay(),yield()등) 호출하기 - 명시적으로 취소 상태 확인하기(
isActive,ensureActive())
- 일시 중단 함수(
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
val job = launch {
var nextPrintTime = startTime
var i = 0
// 계산 집약적인 루프 - 취소를 확인해야 함
while (i < 5 && isActive) { // isActive로 취소 상태 확인
if (System.currentTimeMillis() >= nextPrintTime) {
println("코루틴 실행 중... ${i++}")
nextPrintTime += 500L
}
}
// 대안: ensureActive() 사용
// while (i < 5) {
// ensureActive() // 취소되었으면 CancellationException 발생
// // 계산 작업
// }
}
delay(1300)
println("취소 요청")
job.cancel()
job.join()
println("메인 코루틴 종료")
}
이 예제는 계산 집약적인 루프에서 isActive 프로퍼티를 확인하여 취소에 협력하는 방법을 보여줍니다.
3. 코루틴 취소 방법
- 코루틴을 취소하는 다양한 방법과 각 방법의 특징에 대해 알아보겠습니다.
3.1 Job.cancel() 사용
- 가장 기본적인 취소 방법은
Job.cancel()을 호출하는 것입니다. - 선택적으로 취소 이유를 문자열이나 예외로 전달할 수 있습니다.
fun main() = runBlocking {
val job = launch {
repeat(100) { i ->
println("작업 $i 실행 중...")
delay(100)
}
}
delay(300) // 몇 개의 작업이 실행될 시간 허용
// 기본 취소
// job.cancel()
// 취소 이유 지정
job.cancel("더 이상 필요하지 않음")
// 사용자 정의 예외로 취소
// job.cancel(CancellationException("사용자가 취소 버튼을 클릭함"))
job.join() // 취소 완료 대기
println("취소 후 상태: ${if (job.isCancelled) "취소됨" else "활성"}")
}
이 예제는 cancel() 메서드를 사용하여 코루틴을 취소하는 다양한 방법을 보여줍니다.
3.2 cancelAndJoin() 사용
cancel()과join()을 순차적으로 호출하는 것은 일반적인 패턴입니다.- 이를 단순화하기 위해
cancelAndJoin()확장 함수를 사용할 수 있습니다.
fun main() = runBlocking {
val job = launch {
repeat(100) { i ->
println("