Suspend function은 suspend 수정자가 있는 함수로, 스레드를 차단하지 않고 실행을 일시 중지했다가 동일한 코루틴에서 나중에 재개할 수 있습니다. JetBrains Kotlin Docs, 2025에 따르면, suspend 함수는 코루틴의 기본 구성 요소로, 콜백 없이 비동기성을 제공합니다. 각 suspend 함수는 Continuation 기반의 상태 머신으로 컴파일되어 일시 중단 지점을 효율적으로 관리할 수 있습니다.
핵심 사항
Suspend function은 suspend 키워드로 선언된 함수로, 스레드를 차단하지 않고 하나 이상의 지점에서 실행을 일시 중단할 수 있습니다. 다른 suspend 함수 내에서 suspend 함수를 호출할 때마다 잠재적인 일시 중단 지점이 됩니다.
suspend fun fetchUserData(): User {
val response = httpClient.get("/user")
return parser.parse(response)
}
Kotlin 컴파일러는 이러한 함수를 상태 머신으로 변환합니다. 각 일시 중단 지점(다른 suspend 함수 호출)은 상태(label)가 됩니다. 현재 스레드는 상태 사이에서 해제되고, 대기 중인 작업이 완료되면 다음 상태에서 실행이 재개됩니다.
suspend 함수는 Kotlin 1.3(2018년)에서 코루틴과 함께 실험적 기능으로 등장했으며 Kotlin 1.5(2021년)에서 안정화되었습니다. 그 이전에는 Kotlin/Java에서 비동기 처리가 콜백, RxJava 및 CompletableFuture를 통해 이루어졌습니다. suspend 함수는 선형 구문과 자동 스레드 관리를 갖춘 대안을 제공했습니다.
suspend 함수의 내부 작동 방식을 이해하는 것이 코루틴을 올바르게 다루는 핵심입니다. 일반 함수와 달리, 각 suspend 함수는 Continuation 인터페이스를 가진 클래스로 컴파일됩니다.
Kotlin 컴파일러는 각 suspend 함수의 매개변수 목록 끝에 Continuation 타입의 매개변수를 추가합니다. Continuation은 다음을 포함합니다:
다른 suspend 함수에 대한 두 번의 호출이 있는 suspend 함수가 있다고 가정해 봅시다:
suspend fun process() {
val a = stepOne()
val b = stepTwo(a)
println(b)
}
컴파일러는 이를 레이블이 있는 상태 머신으로 변환합니다:
// Simplified generated code representation
fun process(cont: Continuation<Unit>): Any? {
val cont = cont as ProcessContinuation
when (cont.label) {
0 -> {
cont.label = 1
if (stepOne(cont) == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
}
1 -> {
cont.label = 2
val a = cont.result as TypeA
if (stepTwo(a, cont) == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
}
2 -> {
println(cont.result)
Unit
}
}
}
핵심 관찰: 함수가 COROUTINE_SUSPENDED를 반환하면 현재 스레드가 해제됩니다. 비동기 작업이 완료되면 Continuation.resumeWith가 호출되고 상태 머신은 다음 레이블부터 계속됩니다.
suspend 함수 선언은 fun 앞의 suspend 키워드를 제외하면 일반 함수와 다르지 않습니다. 제한 사항은 단 하나입니다: suspend 함수는 코루틴이나 다른 suspend 함수에서만 호출할 수 있습니다.
suspend fun delayAndReturn(ms: Long): String {
delay(ms)
return "Done after ${ms}ms"
}
이 예제에서 delay도 suspend 함수로, 스레드를 차단하지 않고 지정된 밀리초 동안 코루틴을 일시 중단합니다. 지연 후 실행이 재개됩니다.
fun main() = runBlocking {
val result = delayAndReturn(1000)
println(result)
}
runBlocking은 일반 세계와 코루틴 사이의 브리지를 만듭니다. 람다 내부에서는 모든 suspend 함수를 호출할 수 있습니다.
Kotlin은 함수형 타입의 suspend 버전을 지원합니다 — suspend () -> T 및 suspend (A) -> B. 이를 통해 고차 함수에 비동기 람다를 전달할 수 있습니다:
suspend fun withRetry (
retries: Int = 3,
block: suspend () -> T
): T {
repeat(retries - 1) {
try { return block() }
catch (_: Exception) { delay(100) }
}
return block()
}
withRetry 함수는 suspend 람다를 받아 오류 시 실행을 재시도합니다. 이는 재시도가 있는 네트워크 요청의 일반적인 패턴입니다.
suspend 함수와 일반 함수의 차이점은 단순히 수정자를 추가하는 것 이상입니다. 주요 차이점을 살펴보겠습니다.
| 특성 | 일반 함수 | suspend 함수 |
|---|---|---|
| 실행 스레드 | 완료까지 스레드 차단 | 스레드 해제 후 재개 가능 |
| 컴파일러 매개변수 | 지정된 매개변수만 | 끝에 암시적 Continuation |
| 일반 함수에서 호출 | 가능 | 불가능 |
| 스택 | 물리적 스레드 스택 | 힙의 상태 머신 + 지점 간 물리적 스택 |
| 반환값 | 직접 값 | 값 또는 COROUTINE_SUSPENDED |
| 성능 | 최소 오버헤드 | 상태 머신당 ~수 나노초(Kotlin 1.9+) |
일반 함수에는 Continuation이 없습니다 — 상태를 저장할 곳도, 실행을 재개할 수단도 없습니다. 일반 함수에서 suspend 함수를 호출해야 하는 경우 runBlocking(테스트용) 또는 CoroutineScope.launch(생명주기를 고려한 프로덕션용)를 사용하세요.
Kotlin을 사용한 Android 앱에서 suspend 함수의 세 가지 실제 시나리오를 살펴보겠습니다.
Room은 suspend 함수를 직접 지원합니다 — 쿼리가 자동으로 백그라운드 스레드에서 실행됩니다:
@Dao
interface UserDao {
@Query("SELECT * FROM users WHERE id = :id")
suspend fun getUser(id: Int): User?
@Insert
suspend fun insertUser(user: User)
}
Room은 내부적으로 쿼리 실행을 위해 Dispatchers.IO를 사용하며, 결과는 suspend 함수가 호출된 디스패처에서 반환됩니다.
class ProfileViewModel : ViewModel() {
private val repo = ProfileRepository()
fun loadProfile(id: String) {
viewModelScope.launch {
val profile = repo.getProfile(id)
_profile.value = profile
}
}
}
ViewModelScope.launch는 코루틴을 생성하고, 그 내부에서 getProfile suspend 함수가 호출됩니다. 결과를 받은 후 UI가 메인 스레드에서 업데이트됩니다.
suspend fun placeOrder(cart: Cart): OrderResult {
val validated = validateCart(cart)
val payment = processPayment(validated)
val receipt = sendReceipt(payment)
return receipt
}
세 개의 suspend 함수가 순차적으로 실행됩니다. 각 단계에서 코루틴은 스레드를 차단하지 않고 일시 중단될 수 있습니다. 어떤 단계에서 예외가 발생하면 나머지 단계는 실행되지 않아 잘못된 주문 상태를 방지합니다.
숙련된 Kotlin 개발자도 suspend 함수를 설계할 때 실수를 합니다. 가장 흔한 실수들을 살펴보겠습니다.
suspend 함수가 코드를 자동으로 비동기로 만들지는 않습니다. Thread.sleep(), InputStream.read() 및 기타 차단 호출은 여전히 스레드를 차단합니다. 차단 작업을 감싸려면 withContext(Dispatchers.IO)를 사용하세요.
함수가 다른 suspend 함수를 호출하지 않고 비동기 작업을 수행하지 않는 경우 — suspend 수정자는 불필요합니다. 상태 머신에 오버헤드를 추가하고 호출 컨텍스트를 제한합니다. 함수가 실제로 일시 중단될 때만 suspend로 만드세요.
코루틴이 취소되면 suspend 함수는 CancellationException을 던집니다. 생각 없이 잡지 마세요 — 호출 코드가 취소를 올바르게 완료할 능력을 빼앗게 됩니다. 마무리 작업을 수행해야 하는 경우 finally 블록과 NonCancellable을 사용하세요.
suspend fun safeOperation() {
try {
doWork()
} finally {
withContext(NonCancellable) {
cleanup()
}
}
}
finally 블록은 취소 시를 포함하여 항상 실행되며, NonCancellable은 정리가 중단되지 않도록 보장합니다.
코루틴을 생성하지 않고 콜백에서 직접 suspend 함수를 호출할 수 없습니다. 콜백 스타일을 코루틴에 적용하려면 suspendCoroutine 또는 suspendCancellableCoroutine을 사용하세요.
자주 묻는 질문
네, 기술적으로 suspend 함수는 다른 suspend 함수를 호출하지 않을 수 있습니다. 컴파일러는 단일 상태(label 0)로 상태 머신을 생성합니다. 그러나 이러한 함수에는 실용적인 이점이 없습니다 — 일반 함수처럼 실행되지만 오버헤드가 있습니다. 불필요하게 suspend를 사용하지 마세요.
Kotlin은 kotlinx-coroutines-debug를 제공합니다 — DebugProbes 및 코루틴 추적 도구가 포함된 라이브러리입니다. Arctic Fox부터 Android Studio의 디버거에는 활성 코루틴, 상태 및 일시 중단 지점을 보여주는 Coroutines 탭이 내장되어 있습니다.
각 일시 중단 지점은 상태 머신에 새 상태를 만듭니다. 대부분의 응용 프로그램에서 한 지점의 오버헤드는 수 나노초입니다(Kotlin 1.9+). 루프에서 수만 개의 지점이 있는 경우에만 작업 결합 또는 sequence/flow 사용을 고려해야 합니다.
Kotlin에서 suspend는 함수형 수정자이며 반환값 마커(C#의 async)가 아닙니다. 모든 suspend 함수는 모든 매개변수와 반환 타입을 가질 수 있으며, 호출은 구문적으로 일반 함수 호출과 다르지 않습니다 — 호출 지점에 await 연산자가 없습니다.
적응을 위해 suspendCancellableCoroutine을 사용하세요. 내부에서 continuation.resume()을 호출하는 콜백을 등록하고, 콜백이 구독 취소를 지원하는 경우 취소 토큰을 반환합니다. 이는 오래된 Android API를 래핑하는 표준 패턴입니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.