|

코틀린 suspend 함수는 코루틴과 무엇이 다를까: 멈추는 함수처럼 보이는 이유

코틀린 suspend 함수 코루틴 차이 대표 이미지
suspend 함수는 코루틴 안에서 중단과 재개가 가능한 함수입니다. 코루틴 그 자체는 아닙니다.

코틀린 suspend 함수는 이름 때문에 ‘함수를 멈춘다’거나 ‘스레드를 잠깐 정지한다’는 뜻처럼 보입니다. 하지만 suspend는 코루틴 안에서 중단했다가 나중에 이어서 실행될 수 있다는 표시입니다.

핵심은 suspend는 함수의 실행 문맥을 코루틴과 연결하는 약속이지, 코루틴 그 자체가 아니라는 점입니다. 이 글은 Kotlin coroutine 공식 문서와 kotlinx.coroutines API 설명을 기준으로 suspend 함수의 의미를 정리합니다.

코틀린 suspend 함수와 코루틴 차이 요약 카드
suspend는 함수의 성격이고, coroutine은 그 함수를 실행하는 비동기 작업의 문맥입니다.

코틀린 suspend 함수는 코루틴 그 자체가 아니다

`suspend fun`은 이 함수가 중단될 수 있다는 표시입니다. 코루틴은 그런 suspend 함수를 실행할 수 있는 비동기 작업의 문맥입니다. 그래서 suspend 함수는 혼자 갑자기 백그라운드에서 실행되지 않습니다.

suspend fun loadUser(): User {
    delay(1000)
    return api.fetchUser()
}

위 함수는 suspend 함수지만, 호출한다고 자동으로 새 코루틴이 만들어지는 것은 아닙니다. 이미 코루틴 안에 있거나 다른 suspend 함수 안에 있어야 자연스럽게 호출됩니다.

왜 일반 함수에서 바로 호출할 수 없을까

suspend 함수는 중간에 멈췄다가 나중에 이어질 수 있습니다. 일반 함수는 이런 중단과 재개 문맥을 가지고 있지 않기 때문에 suspend 함수를 바로 호출할 수 없습니다.

fun onClick() {
    // loadUser() // 컴파일 오류
}

fun onClick(scope: CoroutineScope) {
    scope.launch {
        val user = loadUser()
        render(user)
    }
}

`launch`는 새 코루틴을 시작합니다. 그 안에서는 suspend 함수를 순차 코드처럼 호출할 수 있습니다.

delay는 Thread.sleep과 무엇이 다를까

`Thread.sleep`은 현재 스레드를 붙잡고 쉽니다. 반면 `delay`는 현재 코루틴을 중단하고, 스레드를 다른 작업에 쓸 수 있게 합니다.

suspend fun showLoading() {
    println("start")
    delay(1000)
    println("end")
}

이 차이 때문에 suspend 함수는 비동기 코드를 읽기 쉬운 순서형 코드처럼 작성하게 해 줍니다. 하지만 내부에서 blocking IO를 직접 호출하면 suspend라고 해서 자동으로 안전해지는 것은 아닙니다.

blocking과 non-blocking을 섞어 이해하지 말자

suspend 함수의 이름만 보고 모든 작업이 non-blocking이라고 생각하면 위험합니다. 어떤 API는 suspend로 감싸져 있어도 내부 dispatcher가 중요하고, 어떤 작업은 별도 dispatcher로 옮겨야 합니다.

suspend fun readFile(path: Path): String = withContext(Dispatchers.IO) {
    Files.readString(path)
}

파일 IO처럼 blocking 성격이 강한 작업은 `Dispatchers.IO` 같은 적절한 실행 문맥을 사용해야 합니다.

suspend 함수와 Flow는 어떻게 다를까

suspend 함수는 보통 한 번 호출해서 하나의 결과를 받는 작업에 잘 맞습니다. Flow는 시간이 지나며 여러 값을 흘려보내는 작업에 더 잘 맞습니다.

  • 사용자 정보를 한 번 가져오기: suspend 함수가 자연스럽다
  • 검색어 입력 변화 계속 관찰하기: Flow가 자연스럽다
  • 화면 상태 스트림 수집하기: Flow 또는 StateFlow가 자연스럽다

Android 코드에서의 기준

ViewModel 안에서는 repository의 suspend 함수를 코루틴 안에서 호출하는 패턴이 흔합니다. UI 이벤트가 들어오면 ViewModel scope에서 작업을 시작하고, 결과를 상태로 노출합니다.

class UserViewModel(
    private val repository: UserRepository
) : ViewModel() {
    fun refresh() {
        viewModelScope.launch {
            val user = repository.loadUser()
            _uiState.value = UserUiState.Success(user)
        }
    }
}

suspend 함수는 작업의 중단 가능성을 표현하고, CoroutineScope는 그 작업의 생명주기를 관리합니다. 둘을 나눠 보면 구조가 훨씬 선명해집니다.

실무 체크리스트

  1. 한 번의 비동기 결과면 suspend 함수를 우선 검토한다
  2. 계속 흘러오는 데이터면 Flow를 검토한다
  3. suspend 함수는 coroutine 또는 다른 suspend 함수 안에서 호출한다
  4. blocking 작업은 적절한 dispatcher로 옮긴다
  5. scope는 화면, ViewModel, 작업 생명주기에 맞게 고른다

정리

코틀린 suspend 함수를 이해할 때는 문법 이름보다 문제를 일으키는 지점을 먼저 봐야 합니다. 오늘 글의 핵심은 겉으로 비슷해 보이는 코드가 어떤 기준으로 다르게 동작하는지 분리해 보는 것입니다.

함께 보면 좋은 내부 글은 코틀린 Flow와 suspend 함수 차이, 코틀린 coroutineScope와 supervisorScope 차이, Observer 패턴과 Flow 감각 함께 이해하기입니다. 외부 기준은 Kotlin Docs – Coroutines basics, Kotlin Docs – Composing suspending functions, kotlinx.coroutines API – delay를 확인했습니다.

함께보면 좋은 글