[번역: Kotlin Coroutines: Deep Dive] (2) 코틀린 코루틴에서 일시 정지 작업을 하는 방법
이 포스팅은 Kotlin Coroutines: Deep Dive의 글을 번역한 것입니다. 오역, 많은 의역, 생략이 있으니 감안하여 읽어주십시오.
하나의 코루틴을 일시정지하는 것은 중간에 멈추는 것을 의미한다. 이건 세이브한 게임을 멈추고 당신이 다른 일에 집중할 수 있는 상태와 유사하다. 그리고 후에 언제든 세이브포인트에서 게임을 다시 시작할 수 있다. 코루틴은 일시정지하면 Continuation을 반환한다. 이를 사용하여 멈춘 지점에서 다시 시작할 수 있다.
이는 save가 아니라 block하는 스레드와는 매우 다르다는 사실에 주의하라. 코루틴은 매우 강력하다. 일시정지했을 때 어떤 리소스도 사용하지 않는다. 코루틴은 다른 스레드를 다시 시작할 수 있다. (적어도 이론 상으로는) 하나의 continuation은 직렬화, 비직렬화되어있을 수 있고 다시 시작될 수도 있다.
다시 시작
1 | suspend fun main() { |
위 코드를 실행하면 “After” 글자를 볼 수 없을 것이다. 그리고 (main 함수는 절대 끝나지 않기 때문에) 코드가 실행을 멈춘 것도 아니다. 우리는 게임을 멈췄지만 다시 시작하지는 않았다. 그렇다면 어떻게 다시 시작할 것인가? 앞서 언급한 Continuation는 어디에 있는가?
suspendCoroutine 블록의 람다식을 다시 보자. argument를 받는 이 함수는 일시정지하기 전에 불리워진다. 그때의 매개변수가 continuation이다.
1 | suspend fun main() { |
같은 곳에서 다른 함수를 부르는 이러한 함수는 전혀 새로운 것이 아니다. 이는 let이나 apply, useLines와 유사하다. suspendCoroutine 함수는 이와 같은 방식으로 디자인되었다. 즉 일시정지하기 전에 continuation을 사용할 수 있도록 하는 것이다. suspendCoroutine이 호출된 후면 너무 늦을 것이다. 그래서 람다식을 통해 그 전에 부를 수 있도록 한다. 람다는 continuation을 어디서든 저장하거나 이를 다시시작할지 여부를 계획하기도 한다.
즉시 다시 시작하려면 이렿게하면 된다.
1 | suspend fun main() { |
위에서 “After”가 suspendCoroutine 안에서 resume을 호출했기 때문에 출력되었음에 주의하라.
Kotlin 1.3 이후로 Continuation 정의가 바뀌었다. resume과 resumeWithException 대신 하나의 결과값이 있는 resumeWith 함수가 있다. 우리가 사용하고 있는 resume과 resumeWithException 함수는 resumeWith를 사용한 표준 라이브러리에 있는 extension 함수이다.
1 | inline fun <T> Continuation<T>.resume(value: T): Unit = |
설정된 시간동안 sleep할 다른 스레드를 시작할 수도 있다. 이는 언제든지 다시 시작할 수 있다.
1 | suspend fun main() { |
중요한 부분이다. 정의된 시간이 끝난 후에 continuation을 다시 시작할 함수를 만들 수 있음에 주목하라. 여기서 continuation은 아래 보이는 것처럼 람다식에 의해 좌우된다.
1 | fun continueAfterSecond(continuation: Continuation<Unit>) { |
이건 오류 없이 작동은 한다. 하지만 단 일 초 동안만 비활성화 상태가 되었다가 끝나는 스레드를 불필요하게 생성한다. 스레드는 비용이 싸지 않은데도 왜 낭비하고 있는가? 더 좋은 방법은 “알람”을 설정하는 것이다. JVM에서 그것을 구현하기 위해 ScheduledExecutorService를 사용했다. 여기서는 정의된 얼마간의 시간 후에 몇 개의 continuation.resume(Unit)을 호출하는 걸로 알람을 설정할 수 있다.
1 | private val executor = |
설정한 시간동안 일시정지하는 건 유용한 피처로 보인다. 그러니 이걸 delay라는 함수로 추출해보자.
1 | suspend fun delay(timeMillis: Long): Unit = |
executor가 여전히 스레드를 사용하고 있지만 delay 함수를 사용하는 모든 코루틴에 한 개의 스레드만을 할당한다. 이 점은 하나의 스레드를 매번 블록하는 것보다는 낫다.
이는 코틀린 코루틴 라이브러리에서 지원하는 delay의 방식이다. 구현은 더 복잡하지만 필수적인 요소는 이렇다.
어떤 값을 가지고 다시 시작하기
resume 함수에서 Unit을 넘겨주는 거에 대해 걱정할 수도 있다. 그러면서 suspendCoroutine의 매개변수로 Unit을 사용하는 이유가 궁금해질 것이다. 이 두가지가 같다는 사실은 우연이 아니다. Unit은 함수로부터 반환되고, Continuation 타입 파라미터는 제네릭 타입이다.
1 | val ret: Unit = |
suspendCoroutine이 호출될 때, 이 continuation에서 반환되는 타입이 어떤 것인지 명확해진다. 그 타입은 resume을 호출할 때 사용되곤 한다.
1 | suspend fun main() { |
이는 (save-resume) 게임 비유에는 잘 들어맞지 않는다. 저장본을 다시 시작할 때 게임 내에서 어떤 것(값)을 가져올 수 있는 게임을 나는 알지 못한다. 그러나 이 점은 코루틴을 사용하면 완벽하게 이해된다. 종종 우리는 API로부터 네트워크 response를 받는 것처럼 어떤 데이터를 받아오기 위해 대기하곤 한다. 흔한 시나리오다. 스레드는 특정 데이터가 있는 지점에 도달할 때가지 비즈니스 로직을 실행시킨다. 그래서 네트워크 라이브러리에 이를 전달할 것을 요구한다. 코루틴 없이, 이 스레드는 앉아서 기다릴 수밖에 없다. 스레드가 비싼 자원이기 때문에 이는 엄청난 낭비다. 특히 안드로이드의 메인스레드와 같이 중요한 스레드라면 더욱 그렇다. 코루틴을 사용하면 그냥 일시정지 시켰다가, 라이브러리에게 “이 데이터를 받자마자 resume 함수에게 그냥 넘겨”라는 명령과 함께 continuation을 넘기기만 하면 된다. 그러면 스레드는 다른 일을 하러 갈 수 있다. 데이터가 오면 바로 그 스레드는 코루틴이 일시정지한 지점부터 다시 시작할 것이다.
실제로 이를 확인하기 위해 데이터를 수신할 때까지 일시정지하는 방법을 살펴보자. 아래 예제에서는 외부에서 가져온 requestUser 콜백함수를 사용한다.
1 | suspend fun main() { |
suspendCoroutine을 직접 호출하는 것은 편리하지 않다. 대신 suspend 함수를 만드는 것을 선호한다. 그래서 이를 추출할 수도 있다.
1 | suspend fun requestUser(): User { |
현재 suspend 함수는 Retrofit이나 Room과 같은 많은 유명한 라이브러리에서 지원하고 있다. 그물게 suspend 함수 안에 콜백 함수를 쓸 때가 있다. 하지만 그런 경우라면 suspendCoroutine 때신suspendConcellableCoroutine을 사용하는 걸 추천한다. 이는 Cancellation 챕터에서 설명할 것이다.
1 | suspend fun requestUser(): User { |
여기서 API가 데이터가 아닌 다른 문제가 발생했을 때는 어떻게 될지 궁금할 수도 있다. 서비스가 죽거나 오류를 수신할까? 위 예제에서는 데이터를 반환하지 않는다. 대신 코루틴이 일시정지한 곳에서 예외가 발생해야 한다. 예외를 받고 다시 시작한다.
예외를 받은 resume
우리가 호출하는 모든 함수는 어떤 값을 반환하거나 예외를 던진다. suspendCoroutine도 같다. resume이 호출되면 매개변수로 데이터를 넘긴다. resumeWithException이 호출되면 매개변수로 전달된 예외가 일시정지한 지점에 개념적으로 던져진다.
1 | class MyException : Throwable("Just an exception") |
이 매커니즘은 다른 문제에서도 쓰인다. 한 예로, 네트워크 예외 신호를 보낼 때가 있다.
1 | suspend fun requestUser(): User { |
함수가 아닌 코루틴을 일시정지하는 것
하나 강조할 것은 함수가 아닌 코루틴을 일시정지 시킨다는 것이다. 함수를 일시정지할 수 있는 것은 코루틴이 아니라 코루틴을 일시정지할 수 있는 함수이다. 어떤 변수에 함수를 저장하는 상상를 해보아라. 그리고 함수 호출 후 다시 시작시켜라.
1 | // 정말 이렇게 짜지 마세요 |
이건 좀 이해가 안 된다. 이건 게임을 멈추고 이전 지점에서 재시작하는 프로세스와 동일한데도 resume이 전혀 호출되지 않는다. 출력결과에는 오직 “Before”만 보인다. 그리고 다른 스레드나 코루틴에서 resume을 호출하지 않으면 이 프로그램은 영원히 끝나지 않는다. 이를 위해 1초 후 reusme하는 다른 코루틴을 배치할 수도 있다.
1 | // 정말 이렇게 짜지 마세요. 잠재적인 메모리 누수의 원인이 됩니다. |