[Kotlin] Coroutine 취소하기

H43RO·2021년 8월 25일
6

Kotlin 과 친해지기

목록 보기
7/18
post-thumbnail

💡 코틀린 공식 문서를 참고하여 작성한 글입니다

이전 포스팅과 이어집니다! [Kotlin] Coroutine suspend 함수 활용

마! 실행할 줄 알믄 취소할 줄도 알아야제!

코루틴을 여러 개 굴리다보면 백그라운드에서 돌아가는 코루틴도 세밀하게 제어해야 한다. 예를 들어, 사용자는 이미 코루틴이 돌아가는 페이지를 껐는데 백그라운드에서 계속 쓸 데 없는 동작을 하고 있다면 자원 낭비가 발생한다. 따라서, 더 이상 필요하지 않는 코루틴 동작 같은 경우엔 취소를 해주자.

launch 녀석은 Job 객체를 리턴한다고 거듭 강조했었다. 우리는 이 Job 녀석을 통해 코루틴 동작을 취소해볼 수 있다. 바로 Jobcancel() 을 사용하면 된다.

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 ...
메인 : 뭐함..?

컴퓨팅 동작은 코루틴이 취소됨을 감지할 방도가 없다. 그럼 이러한 문제는 어떻게 해결해야 할까?


Computation Code 를 취소 가능한 형태로 만들기

이를 해결하기 위해 두 가지 방법이 존재한다. 첫 번째는 코루틴이 취소됨을 확인하는 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초 뒤)

코루틴 : 꿀 빨고 있었는데 아쉽네 흠..
메인 :... 잘했엉

이렇듯 직관적으로, 명확하게 코루틴 동작을 취소해볼 수 있다!

profile
어려울수록 기본에 미치고 열광하라

3개의 댓글

comment-user-thumbnail
2021년 8월 28일

정보 감사합니다. cancel 한다고 끝이 아니었군요

1개의 답글
comment-user-thumbnail
2024년 3월 27일

헉 정말 술술 읽히고 이해도 잘되는 포스트네요 !
많이 배우고 갑니다 :)

답글 달기