스레드를 블로킹하지 않고 실행을 중단·재개할 수 있는 함수.
suspend fun loadUser(id: Long): User {
val user = api.getUser(id) // 여기서 '멈추지만' 스레드는 놓아 준다
val posts = api.getPosts(id) // 순차적으로 읽힌다
return user.copy(posts = posts)
}
무엇이 다른가
-
블로킹 — 스레드가 멈춰서 기다린다 — 그동안 그 스레드는 아무것도 못 한다
-
suspend — 코루틴이 멈추고 스레드는 다른 코루틴을 실행한다
-
스레드 하나로 수천 개의 코루틴을 돌릴 수 있는 이유
컴파일러가 상태 기계로 변환한다(CPS) — suspend 함수는 내부적으로 "어디까지 실행했는지"를 기억하는 객체가 된다.
시작하는 두 방법
// launch — 결과가 필요 없다. Job 을 반환한다
viewModelScope.launch {
repository.refresh()
}
// async — 결과가 필요하다. Deferred 를 반환하고 await 로 받는다
viewModelScope.launch {
val user = async { api.getUser(id) }
val posts = async { api.getPosts(id) }
render(user.await(), posts.await()) // 둘이 '병렬로' 돈다
}
async 두 개를 만들고 나중에 await해야 병렬이다. 만들자마자 await하면 순차 실행이 된다.
Dispatchers — 어느 스레드에서 도나
- Dispatchers.Main — UI 스레드 (안드로이드)
- Dispatchers.IO — 네트워크·디스크 — 필요에 따라 스레드가 늘어난다 (기본 64개까지)
- Dispatchers.Default — CPU 집약 연산 — 코어 수만큼
- Dispatchers.Unconfined — 특별한 경우 외에는 쓰지 않는다
// 함수 안에서 스스로 컨텍스트를 바꾼다 — 호출자가 신경 쓸 필요 없게
suspend fun load(): Data = withContext(Dispatchers.IO) {
database.query()
}
"suspend 함수는 호출자를 블로킹하지 않을 책임이 있다" 가 관용구다 — 무거운 일을 하면 스스로 withContext로 옮긴다.
예외 처리가 launch와 async에서 다르다
launch { throw E() } // 즉시 전파된다 → CoroutineExceptionHandler 로 잡는다
val d = async { throw E() } // await 할 때까지 미뤄진다 → try-catch 로 잡는다
viewModelScope.launch {
try { repository.refresh() }
catch (e: IOException) { _error.value = e.message }
}
면접 함정
- ❌ "suspend를 붙이면 비동기로 실행된다" → 호출한 코루틴 안에서 순차 실행된다. 병렬은
async나 새launch로 만든다. - ❌ "suspend 함수는 스레드를 바꾼다" → 스스로
withContext를 하지 않으면 호출자의 스레드에서 돈다.
어디서 부를 수 있나
suspend 함수는 '다른 suspend 함수' 나 '코루틴 빌더 안' 에서만 부를 수 있다
일반 함수에서 부르려면
- 코루틴을 시작한다 (launch · async)
- runBlocking (테스트·main 함수에서만. 프로덕션 코드에서는 지양)
@Test fun test() = runTest { // 테스트에서는 runTest
val result = repository.load() // 가상 시간이라 delay 가 즉시 지나간다
assertEquals(expected, result)
}
콜백 API를 suspend로 감싸기
suspend fun getLocation(): Location = suspendCancellableCoroutine { cont ->
val callback = object : LocationCallback() {
override fun onResult(loc: Location) { cont.resume(loc) }
override fun onError(e: Exception) { cont.resumeWithException(e) }
}
client.request(callback)
cont.invokeOnCancellation { client.cancel(callback) } // 취소 시 정리
}
invokeOnCancellation을 빠뜨리면 코루틴이 취소돼도 원래 요청이 계속 돈다.
실무 규칙
- suspend 함수는 메인 세이프하게 만든다 (스스로 withContext 로 옮긴다)
- 호출자가 Dispatcher 를 신경 쓰게 하지 않는다
- ViewModel 은 Dispatcher 를 주입받게 해 테스트 가능하게 한다