“오늘 디노가 수업하신 코루틴이라는거 들어봤어? 진짜 자바 thread 열던거 진짜 구리다니까? 그냥 콜백 지옥이었어 ㅠ”
비비는 숨이 턱끝까지 차오른다. 오늘도 버스를 놓칠까 급하게 뛴 모양이다. 그런 와중에도 골수 우테코 안드로이드 개발자답게 코루틴 이야기를 꺼내며 공백 로스터리 문을 열었다.
⎯ 선릉역의 점심시간답게 가게 안은 활기 넘치는 수다 소리가 들린다.
공백은 카운터 앞에서 코딩하다 말고 반가운 코웃음을 친다.
“뭐야, 나한테는 개발 얘기한다고 뭐라 하더니~ 반가워!”
“진짜라니까. thread? 걔네는 무슨 거의 우리집 앞 빨간버스야. 많이 굴리지도 못하면서 생성 비용도 크고, 쓸데없이 무거워.”
비비는 흐르는 땀을 닦으며 먼저 와 있던 오이 옆에 털썩 앉았다.
“ㅋㅋ! 그래서 다음 미션에서는 코루틴 쓴다고?”
오이는 본인이 우테코 공식 테토남임을 보여주듯 롱블랙 한 잔을 원샷하며 물었다.
“응. 한정된 스레드 풀 위에서 동시성 다 뽑아내. 무겁기만한 스레드 백 개 띄울 바엔 코루틴 천 개 던지는 게 효율이 훨씬 좋다니까? 이건 지오도 앞구르기하고 인정할걸?”
문이 열리고 메다가 들어온다. 손엔 노트북, 다른 손엔 편의점 도시락.
“나도 오늘 하루 종일 suspend fun이었어. 우테코에서 호출은 되는데 코딩하려고 실행은 안돼.”
셋은 장난스레 웃는다. 비비가 손으로 본인의 옆자리를 툭툭 쳐준다.
“메다 앉아봐~ 오늘은 내가 설명해볼게.”
메다가 도시락을 꺼내면서 묻는다.
“아니~ 진짜로! suspend fun은 뭐야? 그냥 멈췄다가 다시 가는 건가?”
비비가 젓가락을 주섬주섬 꺼내는 메다를 보며 말한다.
“거의 맞았어. suspend fun은 진짜 suspend만 붙여줘도 일시정지가 가능한 함수야.
그러니까 ‘그 자리’에서 멈췄다가, 나중에 이어서 실행돼. 코루틴 안에서만 호출할 수 있고.”
공백이 말했다.
“커피 머신으로 치면, 물 끓이는 중간에 온도가 떨어지면 잠깐 멈췄다가, 물 다 끓으면 다시 커피 내리는 함수 같은 거지. 그 과정 전체를 하나의 suspend fun이라고 보면 돼.”
메다가 도시락을 한 입 베어물며 중얼거린다.
“아, 그러니까 일시정지가 가능한 함수라서, 코루틴이랑 찰떡인 거구나.
thread처럼 끝까지 엉덩이 차지하지 않고, 잠깐 비켜주는 느낌.”
비비가 고개를 끄덕인다.
“응. 그리고 중요한 건, suspend fun은 코루틴 안에서만 호출할 수 있다는 거. 코루틴 아니면 컴파일러가 화낸다? 아니 그 전에 메다, 카페 나갈때까지 도시락은 suspend 해줘!”
메다는 고개를 한 번 더 끄덕이고는 도시락 뚜껑을 테이블 옆으로 밀어두며 다시 묻는다.
“그럼… 코루틴 쓰려면 그냥 launch만 쓰면 돼? async는 뭐고 await는 뭐야?”
“launch는 그냥 말 그대로 발사만. 즉, 내부 로직을 실행만 해. 근데 async는 다르다? 값을 돌려줘. await로 기다리면 값을 받을 수도 있어!”
비비는 새로 산 맥북을 꺼내며 신나게 설명을 이어간다.
“join은 launch 썼을 때 기다리는 거고. runBlocking은 메인 스레드 막아놓고 코루틴 테스트를 할 때 주로 쓰고. 메인 스레드를 터뜨릴까봐 실무에선 잘 안 써. 잘못 쓰면 디노한테 혼날걸?”
공백이 다가오며 커피를 내려놓았다.
“너네 얘기 듣다 보니까 카페 직원의 하루도 코루틴인데?”
“어떻게 그러지?” 오이가 물었다.
“주문 들어오면 launch(Dispatchers.IO)로 원두를 저장소에서 꺼내. 그리고 이어서 launch 내부의 withContext(Dispatchers.Main)로 손님한테 결과 출력도 할 수 있잖아. 깔끔하게. 가게도 마찬가지야. 바리스타가 저장소에서 커피를 가져오는 IO, 손님에게 커피를 보여주는 Main.”
공백은 이어서 말했다.
“계산 많은 작업은 Default 쓰면 돼. 예를 들어 매출 통계 내거나, 원두 소비량 분석 같은 거. 그런 건 IO보다 Default가 더 잘 버텨.
IO는 대기 많은 작업에 강하고, Default는 계산량 많은 작업에 강하거든.”
“오, 이제 좀 알 것 같기도 해.” 메다가 고개를 끄덕였다.
“은근 조심해야 할 건 Unconfined야.”
공백은 컵을 들어 보여주며 쓸데없이 비장하게 말한다.
“처음엔 프론트에서 주문 받는 것 같더니, 잠깐 뒤엔 창고 가서 원두 정리하고 있고, 좀 있다가는 탕비실 가서 혼자 커피 타고 있어.
문서 정리해달라고 맡겼더니 엑셀 열기는 했는데, 갑자기 어디서 다시 시작할지 본인도 모르는 거지.
처음엔 메인 스레드에서 일 시작했는데, suspend됐다가 나중에 다시 재개될 땐 완전히 딴 데서 돌아오니까 UI에 뿌릴 타이밍 놓쳐.
그래서 UI 작업엔 절대 안 써. 그냥 콘솔에 로그 찍는 정도? 그런 일만 시켜야 돼.”
비비가 다이스처럼 손가락을 들며 말했다.
“그니까 Unconfined는… 정리하면 디버깅용 티스푼이네.”
공백은 뿌듯한 표정을 지으며 웃는다.
“딱 그 정도. 레시피에 한 티스푼이면 충분해.”
그 순간, 공백의 휴대폰에 슬랙 알림이 뜬다.
두루에게 날아온 리뷰 21개.
공백은 자리에서 힘을 주고 일어나며 말했다.
“또 launch할 시간이네.”
그리고 장난스레 덧붙였다.
“우린 모두 코루틴인가봐. 놀면서 suspend 되어 있다가, 다시 리뷰어분들에 의해 실행되잖아?”
그 말에 이제는 기가 다 빨린 듯 아무도 대답하지 않았다.
미션 리뷰는 여전히 산처럼 쌓였고, 대화는 다음 await를 준비하는 듯 조용해졌다.
📌 Coroutine (코루틴)
// 직원이 코루틴 Scope 안에서 여러 손님의 주문을 관리함
CoroutineScope(Dispatchers.Main).launch {
takeOrders()
}
📌 Thread vs Coroutine
📌 suspend fun
suspend fun brewCoffee(): Coffee {
delay(1000) // 물 끓이는 중
return Coffee("아메리카노")
}
📌 launch
val job = CoroutineScope(Dispatchers.IO).launch {
println("손님 A의 커피 만들기 시작")
delay(500)
println("손님 A 커피 완료")
}
📌 async
val coffee = CoroutineScope(Dispatchers.IO).async {
brewCoffee()
}
val result = coffee.await()
println("손님에게 전달된 커피: ${result.name}")
📌 await()
📌 join()
val job = CoroutineScope(Dispatchers.IO).launch {
delay(1000)
println("손님 B의 커피 완료")
}
job.join() // 다음 주문 전에 기다리기
📌 runBlocking
fun main() = runBlocking {
println("가게 오픈 전 테스트 시작")
brewCoffee()
println("가게 오픈 완료")
}
📌 Dispatchers.IO
📌 Dispatchers.Main
withContext(Dispatchers.Main) {
serveToCustomer(coffee)
}
📌 Dispatchers.Default
CoroutineScope(Dispatchers.Default).launch {
val stats = calculateDailyStats()
println("오늘의 통계: $stats")
}
📌 Dispatchers.Unconfined
CoroutineScope(Dispatchers.Unconfined).launch {
println("이 인턴은 지금 어디에? ${Thread.currentThread().name}")
}
📌 withContext(dispatcher) { }
CoroutineScope(Dispatchers.Main).launch {
// IO에서 커피를 비동기적으로 만든다 (예: DB, 네트워크, 파일 등)
val coffee = withContext(Dispatchers.IO) {
brewCoffee() // suspend fun
}
// Main에서 손님에게 커피를 제공한다 (UI 업데이트)
serveToCustomer(coffee) // UI-safe 함수
}
📌 Job
📌 Deferred
📌 콜백 지옥 (Callback Hell)

📌 구조적 동시성 (Structured Concurrency)
coroutineScope {
launch {
// 직원 A가 처리하는 커피
}
launch {
// 직원 B가 처리하는 커피
}
} // 이 블록이 끝나기 전까지는 둘 다 마무리되어야 함
좋은 글 잘 보고 갑니다~