1. 개요
스레드는 만드는 것도, 유지하는 것도 비싸다. 수천 개의 동시 요청을 스레드 하나씩 배정해서 처리하면 스레드 자체의 메모리와 컨텍스트 스위칭 비용이 병목이 된다. 코루틴은 “스레드를 막지 않고 기다리는” 방법을 언어 차원에서 제공해 이 문제를 줄인다. 이 글은 코루틴 라이브러리 전체를 다루지 않는다. suspend 가 스레드를 어떻게 안 막는지, 그리고 launch/async/coroutineScope 가 왜 묶여 있는지, 개념만 짚는다.
2. 핵심 내용
2-1. suspend 는 “멈출 수 있다” 는 표시다
suspend fun fetchUser(id: Long): User {
delay(100) // Thread.sleep 이 아니다
return api.getUser(id)
}일반 함수를 부르면 그 함수가 끝날 때까지 호출한 스레드가 그 자리에 묶인다. suspend 함수는 다르다. delay(100) 은 100ms 동안 스레드를 점유한 채 기다리지 않는다. 코루틴은 그 지점에서 실행을 스스로 중단(suspend)하고, 스레드를 반납한다. 반납된 스레드는 그동안 다른 코루틴의 작업을 처리한다. 100ms 가 지나면 어떤 스레드가(꼭 원래 그 스레드가 아니어도) 이어서 실행을 재개한다.
suspend 함수는 일반 함수처럼 못 부른다. 반드시 다른 suspend 함수 안에서, 또는 코루틴 빌더(launch, async 등) 안에서만 부를 수 있다. 컴파일러가 “이 함수는 중단될 수 있으니 그 사실을 아는 곳에서만 불러라” 를 강제하는 것이다.
2-2. 스레드 블로킹과 뭐가 다른가
Thread.sleep(100) 은 그 스레드를 100ms 동안 완전히 묶는다. 그 스레드가 다른 일을 하고 싶어도 못 한다. delay(100) 은 스레드를 놔준다. 스레드 하나가 수천 개의 대기 중인 코루틴을 번갈아 처리할 수 있는 이유다. I/O 대기(네트워크 응답, DB 쿼리)처럼 결과를 기다리기만 하고 CPU 는 안 쓰는 구간에서 이 차이가 크다. 자바 쪽에서 스레드 자체를 붙들고 synchronized·volatile로 공유 자원을 지키는 것과는 출발점이 다른, “애초에 스레드를 덜 묶어두는” 접근이다.
2-3. 코루틴을 시작하는 두 가지 — launch, async
coroutineScope {
launch { sendNotification(userId) } // 결과가 필요 없는 작업
val user = async { fetchUser(userId) } // 결과가 필요한 작업
println(user.await())
}launch 는 결과값이 필요 없는 작업을 시작하고 Job 을 돌려준다. async 는 결과값이 필요한 작업을 시작하고 Deferred<T> 를 돌려준다. await() 을 부르면 그 결과를 기다린다(이때도 스레드는 막히지 않는다). 여러 async 를 동시에 띄우고 나중에 await() 하면 순차 실행보다 빨리 끝난다.
2-4. 구조화된 동시성 — coroutineScope
코루틴을 아무 데서나 막 띄우면 “이 코루틴이 언제 끝나는지, 실패하면 누가 아는지” 를 추적할 수 없게 된다. 코틀린은 모든 코루틴이 부모-자식 관계의 스코프에 속하도록 강제한다. 이게 구조화된 동시성이다.
coroutineScope { } 블록은 그 안에서 시작한 모든 자식 코루틴이 전부 끝날 때까지 기다렸다가 블록을 빠져나간다. 자식 중 하나가 예외로 실패하면 나머지 자식도 취소되고, 예외는 부모로 전파된다. “실행은 시켜놓고 결과나 실패를 아무도 신경 안 쓰는” 코루틴이 생기지 않는다. 함수 하나가 스코프를 열고 그 안에서 일어난 모든 일에 대한 책임을 지는 구조다.
반대로 GlobalScope.launch { } 처럼 스코프 없이 코루틴을 띄우면, 그 코루틴은 앱 전체 생명주기와 함께 살아남는다. 호출한 함수가 끝나도, 심지어 실패해도 아무도 모른다. 구조화된 동시성이 의도적으로 막으려는 게 이런 패턴이다.
3. 마무리
요약
suspend함수는 대기 중에 스레드를 반납한다.Thread.sleep과 달리 스레드를 묶어두지 않는다.suspend함수는 다른suspend함수나 코루틴 빌더 안에서만 부를 수 있다.launch는 결과가 필요 없는 작업,async/await는 결과가 필요한 작업에 쓴다.coroutineScope는 모든 자식 코루틴이 끝날 때까지 기다리고, 하나가 실패하면 나머지도 취소한다.- 스코프 없이 코루틴을 띄우면 실행 여부와 실패를 아무도 추적하지 못한다.