본문 바로가기
Android/Coroutine

[Android] 중단(suspend)함수란 무엇이고 어떻게 동작할까? (feat. delay())

by 태크민 2025. 2. 5.

중단(suspend)는 코루틴에서 어떻게 작동되는가?

코루틴을 suspending 한다는 것은, 코루틴을 중간에 일시 정지하는 것을 의미합니다. 이것은 비디오 게임을 중간에 멈췄다가 다시 실행하는 것과 유사합니다.

 

코루틴을 일시 정지 하면 Continuation 객체에게 지금까지 실행한 정보(로컬 변수, 현재 까지 실행된 라인 등)들을 담고, 함수에서 빠져나옵니다. 그리고 다른 함수를 수행하다가 resumeWith() 을 호출 받고 다시 이전 함수를 처리하는 식으로, 일시정지를 구현합니다.

 

 

재개(Resume)

코루틴 내부에서 일시 중지를 하고 싶다면, suspend 함수를 호출해야 일시 중지가 됩니다.

하나의 예를 통해 알아보도록 하겠습니다.

suspend fun main() {
    println("before")

    println("after")
}
//before
//after

 

일반적인 main()에서 println()을 호출하면 당연하게도 결과에 맞게 나옵니다.

 

하지만 이렇게 한다면 어떨까요?

fun main() = runBlocking {
    println("Before")
    suspendCoroutine<Unit> {continuation ->
        println("Suspend")
    }
    println("After")
}
// Before
// Suspend

 

suspendCoroutine()이라는 함수는 콜백 등을 동기화하기 위해 사용합니다.
그렇기에 내부적으로 Continuation 객체의 resumeWith() 호출 하지 않으면, 그 다음 줄이 실행되지 않습니다.

그렇기에 예제 에서는 suspendCoroutine() 이후의 로직은 실행되지 않습니다.

 

그러면 어떻게 중단된 상태를 재개시킬 수 있을까요?

다음과 같이 continiation.resume() 를 호출하여 정지 상태인 suspendCoroutine()를 재개할 수 있습니다.

fun main() = runBlocking {
    println("Before")
    suspendCoroutine<Unit> {continuation ->
        println("Suspend")
        continuation.resume(Unit)
    }
    println("After")
}
// Before
// Suspend
// After

 

코루틴이 일시정지 되고, 다시 돌아와서 그 이후의 로직 부터 실행할 수 있는 이유는 CPS(Continuation Passing Style)라는 개념이 적용 되어 있기 때문입니다. 

CPS에 대한 포스팅을 보지 않으신 분들은 아래 링크를 먼저 참고해주세요.

https://jtm0609.tistory.com/262

 

[Android] 코루틴(Coroutine) 내부적으로 어떻게 동작할까?

코루틴을 사용해보면 약간 궁금한 게 있었다. 어떻게 함수가 중단되고 재개되고 가능한지 의문이 생긴다. 그래서 한번 코루틴 내부를 한번 파헤쳐 보도록 하자.  아래 포스팅한 내용은 KotlinConf

jtm0609.tistory.com

 

우리가 함수에 suspend라는 키워드를 붙이면 컴파일러는 CPS로 사용될 수 있도록 Continuation 파라미터가 함수의 마지막 파라미터로 추가됩니다. 

이렇게 생성된 중단 함수는 코루틴이나 다른 중단 함수 안에서만 호출될 수 있다는 제약이 생기긴 하지만 코루틴이 제공하는 유용한 다른 중단함수들을 사용할 수 있게 된다는 장점 또한 갖게 됩니다.

 

중단 함수는 suspend 키워드 자체가 의미 하듯이 호출될 경우 코루틴의 실행 흐름을 멈추게하는, 다시말해 실행의 분절점이 될 수 있다는 것을 나타냅니다.

이러한 suspend 키워드는 우리가 만든 어떤 함수에든 적용하여 해당 함수를 중단함수로 만들 수 있는데, 그 대상은 예를들어 top-level function, extension function, member function, local function, operator function 등이 될 수 있습니다.

 

다음 예제를 잠깐 살펴봅시다.

fun main(args: Array<String>) = runBlocking {
    (1..2).forEach { num ->
        launch {
            longRunningTask(num, num + 1)
        }
    }
}

suspend fun longRunningTask(input1: Int, input2: Int): Int {
    log("Start calculation. : input1 : $input1, input2 : $input2")
    delay(2000)
    val intermediateResult = input1 + input2
    log("Intermediate result has been calculated (input1 + input2). : $intermediateResult")
    delay(2000)
    val finalResult = intermediateResult * 2
    log("All of the calculation process have done (result * 2). : $finalResult")
    return finalResult
}

private fun log(message: String) {
    println("[${Thread.currentThread().name}] : $message")
}

 

longRunningTask() 중단함수는 두번의 delay() 함수(delay 역시 중단함수)를 호출하는 간단한 함수입니다 (수행이 오래걸리는 함수라고 생각합시다). 이 함수는 main() 함수에서 생성된 코루틴에서 두번 수행됩니다. 이 과정을 그림으로 나타내면 다음과 같습니다.

 

메인함수에서 시작된 LongRunningTask1 (중단함수)은 첫번째 delay() 함수를 만나면 일정 시간 그 수행을 멈춥니다. (이를 중단점(suspension point)라고 부릅시다.) 그러면 다른 멈춰있는 함수에게 실행 기회가 주어집니다. 그러면 LongRunningTask2 가 수행되고 마찬가지로 delay() 함수를 만나 실행을 멈춥니다.

이런식으로 하나의 스레드 안에서 실행 시간을 분할해가며 수행되는 모습이 됩니다.

 

만약 메인 코루틴에서 각각의 중단함수를 호출하지 않고 특정 중단함수가 또 다른 중단함수를 호출하도록 변경한다면 달라지는 부분이 있을까요?

그렇지 않습니다.

코루틴이 중첩될 때처럼 중단함수가 중첩될 때도 Continutation 상태로 호출 정보가 저장되며 마지막 호출까지 완료되면 최초 호출 함수가 그 결과를 받을 수 있습니다.

 

위 그림을 보면 어딘가 많이 본 모습 같지 않나요?

일반적으로 우리가 함수안에서 또다른 함수를 호출하면 스택에 이전 함수포인터를 저장함으로써 현재 함수 수행 후 결과를 돌려줄 위치를 기록해 놓습니다.

이런식으로 A func -> B func -> C func 로 호출 하면 그 정보가 스택에 쌓이고, 다시 C func -> B func -> A func 로 돌아오면서 스택의 정보가 해제 됩니다 (Stack unwinding).
(실제로 Continuation 의 구현체들은 CoroutineStackFrame 이라는 인터페이스 또한 구현하는데 여기에는 호출자 정보(caller stackframe) 또한 가지고 있습니다.)

 

일반적인 함수의 호출은 운영체제에서 그 호출 스택 관리를 해줍니다.

그럼 중첩된 코루틴이나 중첩된 중단함수의 스택관리는 누가 해주는 걸까요?

지금까지 이야기 한 것과 같이 코루틴 프레임워크가 CPS 방식으로 호출 정보(Continuation)를 스택 형태로 유지하고 있다가 호출 스택의 가장 마지막 함수가 실행을 종료하면 결과 값이 직전 호출 함수들로 전파되며 직전 함수를 재개(resume)해 나갑니다.

만약 스택상의 어떤 함수가 예외를 발생시키면 예외 정보를 최초 호출함수까지 Continuation 을 통해 전달합니다.

 

 

delay()함수가 정확히 무엇인가요? 

delay()함수는 현재 코루틴을 지정된 시간동안 일시 중단(suspend)시키는 함수입니다.

중요한 점은 Thread.Sleep()과 같이 스레드를 Blocking하지 않고, 코루틴만 일시 중단한다는 것입니다.

이렇게 함으로써 다른 코루틴이 해당 스레드를 사용할 수 있도록 반환하여, 더 효율적인 비동기 처리가 가능합니다.

 

또한, delay()는 Cancellable한 중단 함수로, 만약 delay() 동작  중에 현재 코루틴의 Job이 취소 되면 즉시, CancellationException을 발생시킵니다. 

만약 특정 시간 동안의 대기가 아닌, 취소될 때까지 무한히 대기하고 싶다면 awaitCancellation() 함수를 사용할 수 있습니다.

 

그렇다면 delay() 내부구조는 어떻게 되어있을까요?

 

delay() 내부를 살펴보면 가장 먼저 등장하는 것이 바로 suspendCancellableCoroutine입니다.

이는 코루틴을 안전하게 일시 중단(suspend) 시키면서, 동시에 취소(cancellation)를 감지할 수 있도록 합니다.

delay()함수는 timeMills 시간 동안 suspendCorotuine을 이용해 현재 코루틴을 일시 중단하고, 지정된 시간이 경과한 후에는 scheduleResumeAfterDay()라는 내부 함수를 통해 코루틴의 재개(resume)을 예약합니다.

또한, 대기시간인 timeMills 시간 동안 코루틴의 job이 취소되었다면, 이를 즉시 감지하고 코루틴을 취소 처리합니다.

 

이러한 동작이 가능한 이유는 delay()함수가 suspendCancellableCoroutine으로 감싸져 있기 때문입니다.

 

suspendCancellable 포스팅을 보지 않으신 분은 아래 링크를 참조해주세요.

https://jtm0609.tistory.com/227

 

[Android] 콜백을 코루틴(Coroutine)으로 바꿔보자(SuspendCoroutine/SuspendCancellableCoroutine)

필자는 서버나, 외부 DB가 필요한 Android 프로젝트에 Firebase를 사용하고 있다. Firebase sdk의 경우 Java 코드로 구현되어 있으며, callback을 통해 비동기를 지원하는 것이 특징이다.필자는 이러한 callback

jtm0609.tistory.com

 

 

delay()와 Thread.Sleep()는 차이가 무엇인가요?

delay()와 Thread.Sleep()는 스레드 차단 방식에서 차이가 있습니다.

 

Thread.Sleep -> 스레드 차단

전통적인 스레드 기반 코드에서는 작업 중간에 Thread.sleep()을 사용하면 해당 스레드는 지정된 시간 동안 차단(blocked)됩니다. 이렇게 되면 해당 스레드는 다른 작업을 처리할 수 없습니다.

Delay() -> 스레드 반환

반면, delay는 코루틴을 일시 중단하면서 스레드를 반환합니다. 이는 비동기 방식으로 처리되며, 스레드가 낭비되지 않고 다른 작업에 사용될 수 있습니다.

 

 

중단(suspend)함수는 어떤게 있나요?

suspend로 선언된 함수는 모두 중단함수입니다.

우리가 자주 사용하는 coroutineScope도 중단 함수입니다. (CoroutineScope가 아닌, coroutineScope인 점 유의)

 

하지만 이들은, 직접적으로 취소를 감지하는 것이 아니라, 내부적으로 delay()와 같이 cancellable 함수를 호출해서 취소 여부를 체크해야합니다. 

 

아래와 같이 말이죠.

suspend fun main() = coroutineScope {

    val job =launch {
        coroutineScope {
            var i =0
            while(true){
                delay(1000)
                println(i++)
            }
        }
    }

    //3초 후 취소
    delay(3000)
    job.cancel()  // 취소 호출
    job.join()
}

/**
0
1
**/

 

만약 delay(1000)를 해주지 않으면 무한정 실행이 될 것입니다.

 

코루틴 라이브러리에서는 아래와 같이 다양한 Cancellable한 중단 함수를 기본으로 제공합니다.

 

1. delay()

 

2. awaitCancellation()

 

3. withContext

 

4. yield()

 

5. suspendCancellableCoroutine

 

6. await()

 

7. join()

 

위에서 언급한 대표적인 중단함수 코드들은 취소가능한(Cancellable) 함수입니다.

즉, delay()뿐만 아니라, 위에서 언급한 중단함수에서 코루틴의 Job 취소 여부를 체크하고 CancellationException을 반환합니다.

 

마무리

결국 코루틴은 suspend 지점에서 자발적으로 실행을 일시 중단하고, delay() 또는 yield()를 통해 다른 작업에 제어권을 양보함으로써 작업 간 전환이 시스템이 아닌 작업 자체의 협력에 의해 이루어집니다.

이를 통해 효율적인 동시성 처리가 가능하며, 이것이 바로 코루틴이 협력형 멀티프로그래밍이라 불리는 이유입니다.


참고자료

https://myungpyo.medium.com/%EC%BD%94%EB%A3%A8%ED%8B%B4-%EA%B3%B5%EC%8B%9D-%EA%B0%80%EC%9D%B4%EB%93%9C-%EC%9E%90%EC%84%B8%ED%9E%88-%EC%9D%BD%EA%B8%B0-part-1-dive-3-b174c735d4fa

 

코루틴 공식 가이드 자세히 읽기 — Part 1 — Dive 3

서스펜드 함수란 무엇이고, 어떻게 동작할까?

myungpyo.medium.com

https://medium.com/@saqwzx88/kt-academy-kotlin-coroutines-deep-dive-summary-1%EB%B6%80-47888289741f

 

Kt.Academy Kotlin Coroutines Deep Dive Summary 1부 — 코루틴 원리 및 빌더

본 게시글은 Kt.Academy의 Kotlin Coroutines DEEP DIVE의 요약본입니다.

medium.com

https://medium.com/@qwertyfairy/note-1-%EC%BD%94%EB%A3%A8%ED%8B%B4%EC%9D%98-delay%EC%99%80-thraed-sleep%EC%9D%98-%EC%B0%A8%EC%9D%B4%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C-4f99c20b60f3

 

Note 1. 코루틴의 delay와 Thraed.sleep의 차이는 무엇일까?

요즘 제일 많이 관심 갖고 공부 하고있는 코루틴에 대해 살짝 끄적여 보려고합니다. 누군가에겐 당연하고 쉬운 지식일지 몰라도 누군가에겐 도움이 될거라 생각이 되었기에 틈틈히 공부하는것

medium.com