티스토리 뷰

728x90

 

중단 함수는 코틀린 코루틴의 핵심이다.

코루틴을 중단한다는 건 실행을 중간에 멈추는 것을 의미한다.

코루틴은 중단되었을 Continuation 객체를 반환한다. Continuation을 이용하면 멈췄던 곳에서 다코루틴을 실행할 있다.

코루틴과 반대로 스레드는 저장이 불가능하고 멈추는 것만 가능하다.

코루틴과 스레드는 확연히 다르다. 코루틴이 훨씬 강력한 도구다.

 

중단했을 코루틴은 어떤 자원도 사용하지 않는다.

코루틴은 다른 스레드에서 시작할 수 있고, Continuation 객체는 직렬화와 역직렬화가 가능하며 다시 실행될 있다.

 

 

재개

중단 함수는 말 그대로 코루틴을 중단할 수 있는 함수다.

즉, 중단 수가 반드시 코루틴이나 다른 중단 함수에 의해 호출되어야 한다.

 

suspend fun main() {
    println("Before")
    suspendCoroutine<Unit> { }
    println("After")
}

두 println() 사이를 중단 지점으로 두기 위해 suspendCoroutine() 함수를 사용했다.

하지만 위 코드를 실행하면 "After"는 출력되지 않고, main()는 멈추지 않고 계속 실행된다.

코루틴은 "Before" 이후에 중단되고, 다시 재개되지 않기 때문이다.

 

그러면 어떻게 다시 실행시킬 있을까?

suspendCoroutine() 함수는 인자로 람다를 받는다. 이 람다는 중단되기 전에 실행되며, Continuation 객체를 인자로 갖는다. 

이 람다를 활용해 코루틴을 재개할 수 있다.

suspend fun main() {
    println("Before")
    suspendCoroutine<Unit> { continuation -> 
        println("Before too")
    }
    println("After")
}
Before
Before too

 

suspendCoroutine() 함수가 호출된 뒤에는 이미 중단되어 Continuation 객체를 사용할 때문에, 람다의 인자로 들어가 중단되기 전에 실행된다.

람다는 코루틴을 다시 실행할 시점을 결정하기 위해 사용된다.

 

suspend fun main() {
    println("Before")
    suspendCoroutine<Unit> { continuation -> 
        continuation.resume(Unit)
    }
    println("After")
}
Before
After

Continuation 객체를 이용해 코루틴을 중단한 후 곧바로 실행할  있다.

 

private vat executor = 
    Executors.newSingleThreadScheduledExecutor {
        Thread(it, "scheduler").apply { isDaemon = true }
    }

suspend fun delay(timeMillis: Long): Unit = 
    suspendCoroutine { cont ->
        executor.schedule({ 
            cont.resume(Unit)
        }, timeMillis, TimeUnit.MILLISECONDS)
    }

suspend fun main() {
    printin("Before")
    delay(1000)
    println("After")
}

위 코드는 코루틴에서 제공하는 delay() 함수가 구현된 방식과 일치한다.

 

물론 스레드를 1초동안 잠자게 하는 방식으로 실행을 멈출 수도 있다.

하지만 만들어진 다음 1초 뒤에 사라지는 스레드는 불필요하다. 스레드를 생성하는 비용은 매우 크다.

더 좋은 방법은 '알람 시계'를 설정하는 것이다.

이를 위해 JVM 제공하는 ScheduledExecutorService를 사용할 수 있다.

정해진 시간이 지나면 continuation.resume(Unit) 호출하도록 알람을 설정한다.

여기서 Executor는 스레드를 사용하긴 하지만, delay 함수를 사용하는 모든 코루틴의 전용 스레드다.

대기할 때마다 하나의 스레드를 블로킹하는 방법보다 훨씬 낫다.

 

 

값으로 재개하기

resume() 함수에 Unit을 인자로 넣을까? suspendCoroutine() 함수타입 인자로 왜 Unit사용할까?

Unit함수의 리턴 타입이며, Continuation 객체제네릭 타입이기도 하다.

 

val ret: Unit =
    suspendCoroutine<Unit> { cont: Continuation<Unit> ->
        cont.resume(Unit)
    }

suspendCoroutine() 함수를 호출할 때 Continuation 객체로 반환될 값의 타입을 지정할 수 있다.

resume() 함수를 통해 반환되는 값은 반드시 지정된 타입과 같은 타입이어야 한다.

 

suspend fun main() {
    val i: Int = suspendCoroutine<Int> { cont ->
        cont.resume(42) 
    }
    println(i) // 42
    
    val str: String = suspendCoroutine<String> { cont ->
        cont.resume("Some text")
    }
    println(str) // Some text
    
    val b: Boolean = suspendCoroutine<Boolean> { cont -> 
        cont.resume(true)
    }
    println(b) // true 
}

API호출해 네트워크 응답을 기다리는 것처럼, 특정 데이터를 기다리려고 중단하기 위해 값으로 재개할 수 있다.

 

스레드는 특정 데이터가 필요한 지점까지 비즈니스 로직을 수행하고, 이후 네트워크 라이브러리를 통해 데이터를 요청한다.

  • 코루틴이 없다면 스레드는 응답을 기다리고 있을 수 밖에 없다.
  • 코루틴이 있으면 중단함과 동시에 "데이터를 받으면resume() 함수로 보내줘."라고 Continuation 객체를 통해 전달한다. 그러면 스레드는 응답을 기다리지 않고 다른 일을 할 있다. 데이터를 받으면 스레드는 코루틴이 중단된 지점에서 재개한다.

 

특정 데이터를 얻을 때까지 중단되는 상황을 예시로 들어보자.

아래 코드는 외부에 구현된 requestUser() 콜백 함수를 사용한다. 

suspend fun main() {
    println("Before")
    val user = suspendCoroutine<User> { cont ->
        requestUser { user -> 
            cont.resume(user)
        } 
    }
    println(user)
    println("After")
}
// Before
// (1초 후)
// User(name=Test)
// After

suspendCoroutine() 함수를 직접 호출하기 보다, 중단 함수를 호출하는 것이 좋다.

아래는 suspendCoroutine() 함수를 중단 함수로 추출한 코드다.

 

suspend fun requestUser(): User {
    return suspendCoroutine<User> { cont ->
        requestUser { user -> 
            cont.resume(user)
        } 
    }
}

suspend fun main() { 
    println("Before")
    val user = requestUser()
    println(user) 
    println("After")
}

 

RetrofitRoom 같은 라이브러리들이 중단 함수를 이미 지원하고 있기 때문에, 중단 함수 내에서 콜백 함수를 사용하는 일은 거의 없다.

만약 필요하다면 suspendCoroutine() 함수 대신에 suspendCancellableCoroutine() 함수를 사용하는 것이 좋다.

(suspendCancellableCoroutine() 함수는 1.9에서 설명)

 

 

예외로 재개하기

외부 서비스(API 등)에서 예외를 던지면 데이터를 반환할 수 없다. 코루틴이 중단된 곳에서 예외를 발생시켜야 한다.

즉, 예외로 재개하는 방법필요하다.

 

suspendCoroutine() 함수도 일반 함수와 마찬가지로 값을 반환하거나 예외를 던진다.

  • resume() 함수가 호출되면 suspendCoroutine() 함수는 인자로 들어온 데이터를 반환한다.
  • resumeWithException() 함수가 호출되면 suspendCoroutine() 함수는 인자로 넣어준 예외를 던진다.
class MyException : Throwable("Just an exception")

suspend fun main() { 
    try {
        suspendCoroutine<Unit> { cont -> 
            cont.resumeWithException(MyException())
        }
    } catch (e: MyException) {
        println("Caught!") 
    }
}

 

네트워크 관련 예외를 던질 때 사용할 수도 있다.

suspend fun requestUser(): User {
    return suspendCancellableCoroutine<User> { cont ->
        requestUser { resp ->
            if (resp.isSuccessful) {
                cont.resume(resp.data)
            } else {
                val e = ApiException(
                    resp.code, 
                    resp.message,
                )
                cont.resumeWithException(e) 
            }
        } 
    }
}

suspend fun requestNews(): News {
    return suspendCancellableCoroutine<News> { cont ->
        requestNews(
            onSuccess = { news -> cont.resume(news) },
            onError = { e -> cont.resumeWithException(e) },
        ) 
    }
}

 

 

함수가 아닌 코루틴을 중단시킨다

중요한 점은 함수가 아닌 코루틴을 중단시킨다는 것이다.

중단 함수는 코루틴이 아니고, 단지 코루틴을 중단할 있는 함수다.

 

변수에 컨티뉴에이션 객체를 저장하고, 함수를 호출한 후 재개하는 예시를 보자.

var continuation: Continuation<Unit>? = null

suspend fun suspendAndSetContinuation() { 
    suspendCoroutine<Unit> { cont ->
        continuation = cont 
    }
}

suspend fun main() { 
    println("Before")
    suspendAndSetContinuation() 
    continuation?.resume(Unit)
    println("After")
}
// Before

여기서 resume은 호출되지 않는다. 이미 suspendCoroutine() 함수가 호출된 후 코루틴이 중단되었기 때문이다.

다른 스레드나 다코루틴으로 재개하지 않으면 프로그램은 끝나지 않고 계속 실행된다.

 

1뒤에 다른 코루틴이 재개하는 코드를 보자. 

var continuation: Continuation<Unit>? = null

suspend fun suspendAndSetContinuation() { 
    suspendCoroutine<Unit> { cont ->
        continuation = cont 
    }
}

suspend fun main() = coroutinescope { 
    println("Before")
    
    launch { 
        delay(1000)
        continuation?.resume(Unit) 
    }

    suspendAndSetContinuation()
    println("After") 
}
// Before
// (1초 후)
// After

의도대로 작동하지만, 메모리 누수가 발생할 수 있기 때문에 이렇게 구현하면 안 된다.

 

 

 

출처

https://www.yes24.com/product/goods/123034354

728x90
250x250
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
글 보관함