깨알 개념/Kotlin

[Kotlin] Coroutines - suspend 키워드

interfacer_han 2024. 2. 14. 10:10

본 게시글의 Coroutine 개념은 Android 내에서 사용되는 것을 전제로 작성되었다.
 

#1 이전 글

 

[Kotlin] Coroutines - 동기 코드, 비동기 코드

#1 동기 코드 vs 비동기 코드 #1-1 구분하기 코루틴을 제대로 사용하기 위해선 먼저, '동기 코드'와 '비동기 코드(= 코루틴 코드)'를 명확하게 구분할 줄 알아야 한다. 둘을 구분하는 기준은 쉽게 말

kenel.tistory.com

해당 게시글을 읽어야 본 게시글을 이해할 수 있다.
 

#2 suspend 키워드

#2-1 suspend와 resume

비동기 코드, 즉 코루틴은 suspend(기다림)이 필요한 코드다. 기다림이 있다는 건 resume(다시 움직임, 재개)도 있다는 말이다. 코루틴을 기다리게(suspend) 만들면, 해당 코루틴의 스택 프레임이 복사되어 별도로 저장된다. 그리고 해당 코루틴을 재개(resume)시키면, 스택 프레임을 다시 불러오고 그 스택 프레임이 저장되었던 곳에서 코루틴이 다시 시작된다. 
 

#2-2 기본 Suspending Function

스레드를 기다리게 만드는 함수 즉 'Suspending Function'은 다음과 같다.

coroutineScope()
supervisorScope()
delay()
join()
await()
receive()
withContext()
withTimeout()
withTimeoutOrNull()

이 함수들은 코루틴 기본 라이브러리(kotlinx.coroutines)의 Suspending Function들이다. 이 기본 함수들은 #2-3와 달리 암시적으로 suspend 키워드가 적용되어져 있다. 여담으로, Room이나 Retrofit같은 라이브러리에선 기본 Suspending Fuction들을 추가로 제공해준다고 한다.

여담으로, runBlocking은 기다리게(Suspend) 만들지 않고, 멈추게(Block) 만드는 것이므로, 위 명단에 없다. 그런데 그 전에, runBlocking은 애초에 동기 코드라서 더더욱 명단에 오를 수 없다.
 

#2-3 사용자 정의 Suspending Function

import kotlinx.coroutines.*

suspend fun suspendingFunction() {
    println("suspendingFunction() called")
    delay(1000) // 1초 대기
    
}

fun main() {
    println("Start")
    
    runBlocking {
        suspendFunction()
    }
    
    println("End")
}

/* 출력 결과
Start
suspendingFunction() called
End
*/

사용자 정의 함수를 Suspending Function으로 만들 수도 있는데, #2-2와 달리 명시적으로 suspend 키워드를 적어야 한다. 함수 키워드인 fun 앞에 suspend 키워드를 붙여주면 된다.
 

#2-4 Suspending Function의 제약

#2-2 또는 #2-3의 Suspending Function은 비동기 코드의 영역(이 링크의 #2 참조)이 되며, 그 영역 안에서 Suspend Function을 비롯한 코루틴 API 코드를 마음껏 사용할 수 있다. 하지만 제약도 생긴다. 앞으로 해당 함수는 동기 코드의 영역(이 링크의 #2 참조)에서 호출할 수 없다. 동기 코드와 비동기 코드의 영역을 구분하는 포장지 역할인 CoroutineScope 등을 제외하면, Suspend 함수는 오직 Suspend 함수 내에서만 호출할 수 있다.

#3 suspend 키워드의 작동 기제

#3-1 샘플 코틀린 클래스

// package com.example.coroutinesbasics

import kotlinx.coroutines.delay

class SuspendPractice {
    fun normalFunction() {
        println("This is normalFunction")
    }

    suspend fun suspendingFunction() {
        println("This is suspendingFunction")
        delay(1000)
    }
}

이 코틀린 클래스를 이용해 suspend 키워드가 작동하는 내부적인 기제를 살펴본다.
 

#3-2 Bytecode 생성

suspend 키워드의 작동 기제를 살펴보기 위해서, 먼저 #3-1의 샘플 코틀린 클래스가 Bytecode를 생성하도록 빌드시킨다. [Build] - [Rebuild Project]를 클릭한다.
 

[Tools] - [Kotlin] - [Show Kotlin Bytecode]로 생성했던 Kotlin Bytecode를 본다
 

#3-3 Decompile

#3-2를 통해 연 Kotlin Bytecode를 우리가 읽을 수 있는 형태로 변환해야 한다. Decompile 버튼을 클릭한다.
 

// package com.example.coroutinesbasics;

import kotlin.Metadata;
import kotlin.Unit;
import kotlin.coroutines.Continuation;
import kotlin.coroutines.intrinsics.IntrinsicsKt;
import kotlinx.coroutines.DelayKt;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

@Metadata(
   mv = {1, 9, 0},
   k = 1,
   d1 = {"\u0000\u0014\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0002\b\u0003\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\u0006\u0010\u0003\u001a\u00020\u0004J\u0011\u0010\u0005\u001a\u00020\u0004H\u0086@ø\u0001\u0000¢\u0006\u0002\u0010\u0006\u0082\u0002\u0004\n\u0002\b\u0019¨\u0006\u0007"},
   d2 = {"Lcom/example/coroutinesbasics/SuspendPractice;", "", "()V", "normalFunction", "", "suspendingFunction", "(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;", "app_debug"}
)
public final class SuspendPractice {
   public final void normalFunction() {
      String var1 = "This is normalFunction";
      System.out.println(var1);
   }

   @Nullable
   public final Object suspendingFunction(@NotNull Continuation $completion) {
      String var2 = "This is suspendingFunction";
      System.out.println(var2);
      Object var10000 = DelayKt.delay(1000L, $completion);
      return var10000 == IntrinsicsKt.getCOROUTINE_SUSPENDED() ? var10000 : Unit.INSTANCE;
   }
}

Decompile 버튼을 누르면 위와 같은 SuspendPractice.decompiled.java 파일이 만들어진다. 이 파일은 우리가 SuspendPractice.kt를 기반으로 생성했던 코틀린 바이트코드를 Java 언어로 해석(Interpretation)한 것이다. suspend 함수에 Continuation 타입의 매개변수가 들어가 있는 것이 보인다. Continuation 인터페이스는 코루틴 코드가 suspend(일시 정지)되었다가 나중에 다시 resume(재개)될 때의 동작을 관리한다. 단어의 뜻처럼 말 그대로 연속(Continuation)의 관리다.

#4 요약

suspend 키워드는 해당 함수가 비동기 코드의 영역임을 명시적으로 표시한다. 프로그래머에게 그리고 컴파일러에게.