코루틴 쉽게 이해하기 (feat. 공백 로스터리)

GongBaek·2025년 5월 30일
post-thumbnail

📕 공백 로스터리의 하루

“오늘 디노가 수업하신 코루틴이라는거 들어봤어? 진짜 자바 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

  • Thread는 손님 하나를 위해 바리스타 한 명을 아예 붙여놓는 구조다.
  • 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

  • 지시 없이 맘대로 움직이는 인턴.
  • 처음엔 프론트에 있다가, 물어볼 때마다 자리를 옮겨 다님. 예측이 안 됨.
  • UI 작업은 시키지 말고 로그나 테스트에서만 쓰자.
CoroutineScope(Dispatchers.Unconfined).launch {
    println("이 인턴은 지금 어디에? ${Thread.currentThread().name}")
}

📌 withContext(dispatcher) { }

  • 바리스타가 원두 꺼낼 땐 IO 역할을 하다가, 손님에게 커피 줄 땐 Main 역할로 컨텍스트를 전환하는 상황이다.
  • 단, withContext는 새로운 코루틴을 여는 것이 아닌 기존의 코루틴을 잠시 전환하는 것이다.
CoroutineScope(Dispatchers.Main).launch {
    // IO에서 커피를 비동기적으로 만든다 (예: DB, 네트워크, 파일 등)
    val coffee = withContext(Dispatchers.IO) {
        brewCoffee() // suspend fun
    }

    // Main에서 손님에게 커피를 제공한다 (UI 업데이트)
    serveToCustomer(coffee) // UI-safe 함수
}

📌 Job

  • 바리스타가 커피 하나 만들기 시작할 때 발급되는 작업 티켓.
  • 완료될 때까지 기다리거나(cancel/join), 중간에 중단시킬 수 있다.

📌 Deferred

  • async를 통해 생성된 커피 영수증 같은 것.
  • 나중에 기다려서 커피가 어떤 건지 받을 수 있다.

📌 콜백 지옥 (Callback Hell)

  • 바리스타가 “커피 다 되면 시럽 뿌려야 하고, 그다음에 컵에 담고, 그다음에…” 하고 지시를 계속 중첩해서 쌓는 구조.
  • 코드가 오른쪽으로 쭉쭉 밀리는 패턴으로 유지 보수가 힘들다.
  • 코루틴은 이런 구조를 직관적인 절차형 코드로 바꿔준다.

📌 구조적 동시성 (Structured Concurrency)

  • 카페에서 직원이 알아서 커피를 만들고, 만약 직원이 퇴근하면 그가 처리 중이던 모든 주문도 함께 종료되는 구조다.
  • 부모-자식 관계가 명확하게 잡히고, 리소스 누수나 메모리 누수가 줄어든다.
  • 코틀린 인 액션에서는 코루틴으로 메모리 누수를 억지로 만드는게 더 힘들다고까지 표현한다.
coroutineScope {
    launch {
        // 직원 A가 처리하는 커피
    }
    launch {
        // 직원 B가 처리하는 커피
    }
} // 이 블록이 끝나기 전까지는 둘 다 마무리되어야 함
profile
Junior Android Developer

2개의 댓글

comment-user-thumbnail
2025년 6월 2일

좋은 글 잘 보고 갑니다~

답글 달기
comment-user-thumbnail
2025년 6월 2일

이해가 정말 쏙쏙 되네요~

답글 달기