💡 코틀린 공식 문서를 참고하여 작성한 글입니다
코루틴을 여러 개 굴리다보면 백그라운드에서 돌아가는 코루틴도 세밀하게 제어해야 한다. 예를 들어, 사용자는 이미 코루틴이 돌아가는 페이지를 껐는데 백그라운드에서 계속 쓸 데 없는 동작을 하고 있다면 자원 낭비가 발생한다. 따라서, 더 이상 필요하지 않는 코루틴 동작 같은 경우엔 취소를 해주자.
launch
녀석은 Job
객체를 리턴한다고 거듭 강조했었다. 우리는 이 Job
녀석을 통해 코루틴 동작을 취소해볼 수 있다. 바로 Job
의 cancel()
을 사용하면 된다.
fun main() = runBlocking {
val job = launch {
repeat(1000) { i ->
println("코루틴 : 저 노가리 좀 깔게요 ㅋㅋ $i ...")
delay(500L)
}
}
delay(1300L)
println("메인 : 코루틴아 좀 꺼져봐")
job.cancel() // Job 취소
job.join() // Job 끝날 때 까지 대기
println("메인 : 굿 ㅋㅋ")
}
코루틴 : 저 노가리 좀 깔게요 ㅋㅋ 0 ...
코루틴 : 저 노가리 좀 깔게요 ㅋㅋ 1 ...
코루틴 : 저 노가리 좀 깔게요 ㅋㅋ 2 ...
메인 : 코루틴아 좀 꺼져봐
메인 : 굿 ㅋㅋ
cancel()
과join()
을 한 번에 할 수 있는cancelAndJoin()
이라는 메소드도 제공해준다! (꿀팁)
위에서 소개한 job.cancel()
등을 이용하여 코루틴을 취소하려면, 서로 간의 협조가 필요하다. kotlinx.coroutines
에 정의되어 있는 모든 Suspending Function 들은 Cancellable (취소 가능한) 형태이다. 이 녀석들은 코루틴 동작의 취소 요청이 들어왔을 때 CancellationException
이라는 오류를 쓰로잉한다. 하지만, 만약 코루틴이 컴퓨팅 동작을 하고 있고 자기가 취소되는지 체크을 하지 않는 상황을 살펴보자.
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 5) {
// 1초에 두 번 노가리 까는 코루틴
if (System.currentTimeMillis() >= nextPrintTime) {
println("코루틴 : 저 노가리 좀 깔게요 ㅋㅋ ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L)
println("메인 : 코루틴아 좀 꺼져봐")
job.cancelAndJoin() // Job 취소하고, 끝날 때 까지 대기
println("메인 : 뭐함..?")
}
이를 실제로 구동해봤을 때, 코루틴을 취소했을 때 바로 코루틴이 끝나지를 않는다.
코루틴 : 저 노가리 좀 깔게요 ㅋㅋ 0 ...
코루틴 : 저 노가리 좀 깔게요 ㅋㅋ 1 ...
코루틴 : 저 노가리 좀 깔게요 ㅋㅋ 2 ...
메인 : 코루틴아 좀 꺼져봐
코루틴 : 저 노가리 좀 깔게요 ㅋㅋ 3 ...
코루틴 : 저 노가리 좀 깔게요 ㅋㅋ 4 ...
메인 : 뭐함..?
컴퓨팅 동작은 코루틴이 취소됨을 감지할 방도가 없다. 그럼 이러한 문제는 어떻게 해결해야 할까?
이를 해결하기 위해 두 가지 방법이 존재한다. 첫 번째는 코루틴이 취소됨을 확인하는 Suspending Function 을 계속 주기적으로 호출하는 방법이다. 딱 이 목적에 걸맞는 Yield
함수라는 것이 있다. (추후 포스팅에서 자세히 알아보겠다)
그리고 나머지 하나는, 취소 상태를 명확하게 체크하는 방법이다. 아래 코드를 실행해보자. 달라진 것은 단 하나, 전부입니다 while
문의 조건에 isActive
라는 변수가 들어있다.
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (isActive) { // 취소 가능한 형태의 루프
// 1초에 두 번 노가리 까는 코루틴
if (System.currentTimeMillis() >= nextPrintTime) {
println("코루틴 : 저 노가리 좀 깔게요 ㅋㅋ ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L)
println("메인 : 코루틴아 좀 꺼져봐")
job.cancelAndJoin() // Job 취소하고, 끝날 때 까지 대기
println("메인 : 굿 ㅋㅋ")
}
코루틴 : 저 노가리 좀 깔게요 ㅋㅋ 0 ...
코루틴 : 저 노가리 좀 깔게요 ㅋㅋ 1 ...
코루틴 : 저 노가리 좀 깔게요 ㅋㅋ 2 ...
메인 : 코루틴아 좀 꺼져봐
메인 : 굿 ㅋㅋ
의도대로 잘 동작했다! isActive
프로퍼티는 CoroutineScope
객체가 자체적으로 지원하는 프로퍼티로, 코루틴의 취소 상태를 Boolean 타입으로 갖고 있는 녀석이다. 따라서 위와 같은 동작이 가능한 것이다.
'취소 가능한' Suspending Function 의 경우, 취소 요청이 들어왔을 때 CancellationException
을 쓰로잉한다. 이는 일반적으로 try-finally
문 등으로 핸들링할 수 있는 오류이다.
fun main() = runBlocking {
val job = launch {
try {
repeat(1000) { i ->
println("코루틴 : 저 노가리 좀 깔게요 ㅋㅋ $i ...")
delay(500L)
}
} finally {
println("코루틴 : 꿀 빨고 있었는데 아쉽네")
}
}
delay(1300L)
println("메인 : 코루틴아 좀 꺼져봐")
job.cancelAndJoin()
println("메인 : 굿 ㅋㅋ")
}
코루틴 : 저 노가리 좀 깔게요 ㅋㅋ 0 ...
코루틴 : 저 노가리 좀 깔게요 ㅋㅋ 1 ...
코루틴 : 저 노가리 좀 깔게요 ㅋㅋ 2 ...
메인 : 코루틴아 좀 꺼져봐
코루틴 : 꿀 빨고 있었는데 아쉽네
메인 : 굿 ㅋㅋ
job.cancelAndJoin()
이 호출되었기 때문에, 코루틴 취소는 물론이고 코루틴 내부의 finally
문까지 실행이 보장된 것을 확인할 수 있다.
위 예제에서 알 수 있듯,
finally
은 절대 취소되지 않는 영역이다. 따라서, 취소 요청이 발생했을 때 안전하게 코루틴 동작을 끝마칠 수 있다. (리소스 할당 해제, 파일 닫기 등)
매우 드문 상황이지만, 취소된 코루틴에서 suspend
동작이 필요할 경우, withContext(NonCancellable)
이라는 스코프를 선언하여 취소되지 않는 Suspending 동작을 수행할 수 있다. 아래와 같이 말이다!
fun main() = runBlocking {
val job = launch {
try {
repeat(1000) { i ->
println("코루틴 : 저 노가리 좀 깔게요 ㅋㅋ $i ...")
delay(500L)
}
} finally {
withContext(NonCancellable) {
println("코루틴 : 나 진짜 1초만 ㅋㅋㅋ 아직 뭐 좀 더 해야돼")
delay(1000L)
println("코루틴 : 꿀 빨고 있었는데 아쉽네 흠..")
}
}
}
delay(1300L)
println("메인 : 코루틴아 좀 꺼져봐")
job.cancelAndJoin()
println("메인 : 음 ... 잘했엉")
}
코루틴 : 저 노가리 좀 깔게요 ㅋㅋ 0 ...
코루틴 : 저 노가리 좀 깔게요 ㅋㅋ 1 ...
코루틴 : 저 노가리 좀 깔게요 ㅋㅋ 2 ...
메인 : 코루틴아 좀 꺼져봐
코루틴 : 나 진짜 1초만 ㅋㅋㅋ 아직 뭐 좀 더 해야돼
(1초 뒤)
코루틴 : 꿀 빨고 있었는데 아쉽네 흠..
메인 : 음 ... 잘했엉
이렇듯 직관적으로, 명확하게 코루틴 동작을 취소해볼 수 있다!
정보 감사합니다. cancel 한다고 끝이 아니었군요