(😨 였λ₯˜ μ •μ •) πŸ€” 2025년도에도 κ°œλ°œμžλ“€μ€ 코루틴 μ˜ˆμ™Έ 처리 λ•Œλ¬Έμ— λ°€μƒ˜κ°? 😨 (2025학년도 μ•ˆλ“œλ‘œμ΄λ“œ νƒκ΅¬μ˜μ—­ λ¬Έμ œν’€μ΄)

(😨 였λ₯˜ μ •μ •) πŸ€” 2025년도에도 κ°œλ°œμžλ“€μ€ 코루틴 μ˜ˆμ™Έ 처리 λ•Œλ¬Έμ— λ°€μƒ˜κ°? 😨 (2025학년도 μ•ˆλ“œλ‘œμ΄λ“œ νƒκ΅¬μ˜μ—­ λ¬Έμ œν’€μ΄)



개인 κ΄‘κ³  μ˜μ—­

이 글은 2025 μ•ˆλ“œλ‘œμ΄λ“œ 탐ꡬ μ˜μ—­μ— λ‚˜μ˜¨ 문제 쀑 일뢀λ₯Ό ν•΄μ„ν•˜λŠ” κΈ€μ˜ ν˜•νƒœλ‘œ μž‘μ„±ν•©λ‹ˆλ‹€. 문제 전체λ₯Ό 담지 μ•Šκ³ , μ€‘μš”ν•œ 해섀을 μž‘μ„±ν•©λ‹ˆλ‹€.

μ •μ •

24.12.11 해석에 였λ₯˜κ°€ 있으며, μ½”λ“œ 검증 κ³Όμ •μ—μ„œ 였λ₯˜λ₯Ό ν™•μΈν•˜μ—¬ μˆ˜μ •ν•©λ‹ˆλ‹€.

λŒ“κΈ€λ‘œ 였λ₯˜κ°€ μžˆλ‹€κ³  μ•Œλ €μ£Όμ‹  Larry, μ†‘μ€€μ˜λ‹˜ κ°μ‚¬ν•©λ‹ˆλ‹€.

μ•ˆλ“œλ‘œμ΄λ“œ 탐ꡬ μ˜μ—­ ν›„κΈ° κΈ€

μ–΄λ–€ 문제일까?

코루틴 Exception λ°œμƒ μ‹œ μ˜ˆμ™Έ λ²”μœ„λ₯Ό λ¬Όμ–΄λ³΄λŠ” μ§ˆλ¬Έμ— λŒ€ν•œ 해석을 λ‹΄λŠ” 글이닀.

λŒ€λž΅ 적어보면

- μ΅œμƒμœ„ Job AλŠ” viewModelScope.launch둜 μƒμ„±λ˜κ³  λ‚΄λΆ€μ—μ„œ B, Cλ₯Ό μƒμ„±ν•œλ‹€.
- Bμ—μ„œλŠ” coroutineScope λ‚΄μ—μ„œ D, Eλ₯Ό μƒμ„±ν•œλ‹€.
- Cμ—μ„œλŠ” withContext(Dispatchers.IO) λ‚΄μ—μ„œ Fλ₯Ό μƒμ„±ν•œλ‹€.

λͺ¨λ“  Job이 Finish λ˜μ—ˆλ‹€κ³  κ°€μ •

A-FκΉŒμ§€ λͺ¨λ‘ Job이 λ¦¬ν„΄λœλ‹€λŠ” 사싀과 λͺ¨λ“  Job이 λ™μž‘ μ™„λ£Œλ˜μ—ˆμ„ λ•Œλ₯Ό κ°€μ •ν•œλ‹€.

이 뢀뢄에 λŒ€ν•œ 글은 이미 과거에도 μž‘μ„±ν–ˆμ–΄μ„œ 링크λ₯Ό 좔가해두겠닀.

이 두 개의 글을 μ΄ν•΄ν•œλ‹€λ©΄ 사싀 해석할 ν•„μš”λ„ μ—†μ§€λ§Œ, μƒˆλ‘œμš΄ 마음으둜 글을 적어본닀.

이 κΈ€μ—μ„œλŠ”

  • 2025 μ•ˆλ“œλ‘œμ΄λ“œ 탐ꡬ μ˜μ—­μ— λ‚˜μ˜¨ 문제 일뢀λ₯Ό μ •λ¦¬ν•œλ‹€.
  • Job에 λŒ€ν•œ 이해가 ν•„μš”ν•˜λ‹€.

문제의 해석

λͺ¨λ“  μ‹€ν–‰μ—μ„œ Job을 가지렀면 launch {}λ₯Ό 톡해 μƒˆλ‘œμš΄ μž‘μ—…μ„ μ‹€ν–‰ν•΄μ•Ό ν•œλ‹€λŠ” 점이닀.

asyncλŠ” DeffDeferred Tλ₯Ό λ¦¬ν„΄ν•˜κΈ°μ— μ μ ˆν•˜μ§€ μ•Šμ€ 싀행에 ν•΄λ‹Ήν•˜κΈ°μ— μ½”λ“œλ‘œ μž‘μ„±ν•˜λ©΄ μ•„λž˜μ™€ κ°™λ‹€.

// A의 μ‹€ν–‰
viewModelScope.launch {
    // B의 μ‹€ν–‰
    launch {
        coroutineScope {
            // D의 μ‹€ν–‰
            launch { }
            // E의 μ‹€ν–‰
            launch { }
        }
    }
    // C의 μ‹€ν–‰
    launch {
        withContext(Dispatchers.IO) {
            // F의 μ‹€ν–‰
            launch { }
        }
    }
}

이 문제λ₯Ό 해석할 λ•Œ μ£Όμ˜ν•  점이 ν•˜λ‚˜ μžˆλ‹€.

Coroutine builder둜 μ‹€ν–‰ν•˜λŠ” 경우 λΆ€λͺ¨μ˜ CoroutineContextλŠ” ν•˜μœ„(μžμ‹) λͺ¨λ‘ μƒμ†λ°›λŠ” 것이 μ•„λ‹ˆλΌλŠ” 점이닀. μ½”λ“œλ₯Ό μΆ”μ ν•˜λ©΄ foldλ₯Ό 톡해 ν•©μ‚°ν•œλ‹€.

단, Job은 상속받지 μ•ŠλŠ”λ° λ‹€μŒκ³Ό 같은 이유둜 μ œλ―Έλ‚˜μ΄μ˜ μ„€λͺ…이닀.

λ§Œμ•½ Job이 μƒμ†λœλ‹€λ©΄, λΆ€λͺ¨ μ½”λ£¨ν‹΄μ˜ Job이 μ·¨μ†Œλ  λ•Œ μžμ‹ 코루틴도 ν•¨κ»˜ μ·¨μ†Œλ©λ‹ˆλ‹€. μ΄λŠ” 각 μ½”λ£¨ν‹΄μ˜ 생λͺ…μ£ΌκΈ°λ₯Ό κ°œλ³„μ μœΌλ‘œ κ΄€λ¦¬ν•˜κΈ° μ–΄λ ΅κ²Œ λ§Œλ“­λ‹ˆλ‹€. 각 코루틴이 독립적인 Job을 가지도둝 ν•¨μœΌλ‘œμ¨, μ½”λ£¨ν‹΄μ˜ μ·¨μ†Œμ™€ 생λͺ…μ£ΌκΈ° 관리λ₯Ό λ”μš± μœ μ—°ν•˜κ²Œ μ œμ–΄ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

κ²°κ΅­ ν•„μš”ν•œ κ²ƒλ§Œ ν•©μ‚°ν•˜κ³  μΉ˜ν™˜ν•  수 있으며, Job은 μ˜ˆμ™Έλ‘œ 상속받지 μ•ŠλŠ”λ‹€.


함정도 μ°Ύμ•„λ³΄μž.

이 λ¬Έμ œμ—λŠ” 2가지 함정이 μžˆλ‹€. λ°”λ‘œ coroutineScopeκ³Ό withContext(Dispatchers.IO)이닀.

이 μ½”λ“œμ—μ„œλŠ” 사싀 μ•„λ¬΄λŸ° μ˜λ―Έκ°€ μ—†λ‹€. μ–΄μ°¨ν”Ό launch {}λ₯Ό 톡해 μ½”λ“œλ₯Ό μ‹€ν–‰ν•˜κΈ° λ•Œλ¬Έμ— blocking을 λ§Œλ“œλŠ” coroutineScopeκ³Ό withContext(Dispatchers.IO) μ½”λ“œμ˜ μ‚¬μš©μ€ μ˜λ―Έκ°€ μ—†κ³  함정을 λ§Œλ“€κΈ° μœ„ν•œ μ½”λ“œ μ„€λͺ…μ΄λΌλŠ” 점이닀.


Job?

Jobμ—λŠ” 크게 2개 μžˆλ‹€.

κ·Έλƒ₯ Job - linkκ³Ό SupervisorJob - link을 가진닀.

이 λ‘˜μ€ λ”± ν•˜λ‚˜μ˜ 차이가 μžˆλŠ”λ°, ν•˜μœ„ Jobμ—μ„œ λ°œμƒν•˜λŠ” Exception μΌ€μ΄μŠ€λ‘œ 인해 λΆ€λͺ¨κ°€ 영ν–₯을 λ°›λŠ”κ°€ μ•„λ‹Œκ°€μ΄λ‹€.

이전 κΈ€μ—μ„œλ„ μž‘μ„±ν–ˆμ§€λ§Œ Kotlin Coroutines Exception 영ν–₯도 μ•Œμ•„λ³΄κΈ°λ₯Ό 쀄이기 μœ„ν•΄μ„œλŠ” SupervisorJob을 ν™œμš©ν•˜λŠ” 것이 μ ν•©ν•˜λ‹€.

λ‹€ν–‰νžˆ android viewModelScope은 SupervisorJob을 기본으둜 ν™œμš©ν•˜κ³  μžˆλ‹€.

Job은 라이프 사이클도 가지고 μžˆλŠ”λ° μ•„λž˜μ˜ κ·Έλ¦Όκ³Ό κ°™λ‹€.

sample_01

이 뢀뢄을 κΌ­ κΈ°μ–΅ν•  μ΄μœ λŠ” μ—†μ§€λ§Œ cancel/fail 처리 μ‹œ Cancelled μƒνƒœλ‘œ λ³€κ²½λœλ‹€λŠ” 점이 μ€‘μš”ν•œ 뢀뢄이닀.

이걸 μ•Œμ§€ μ•Šλ”λΌλ„ μ½”λ£¨ν‹΄μ—μ„œλŠ” exception λ°œμƒ μ‹œ runCatch만 잘 걸어도 λ¬Έμ œκ°€ μ—†μœΌλ©°, 였λ₯˜κ°€ λ°œμƒν•˜λ”λΌλ„ μž¬μ‹œλ„ κ°€λŠ₯ν•˜λ‹€λŠ” 점이 μ€‘μš”ν•œ 포인트 μ•„λ‹κΉŒ μ‹Άλ‹€.


문제 해석 μ •μ •.

μœ„ λ‚΄μš©μœΌλ‘œ μ•Œ 수 μžˆλŠ” 사싀은 κ·Έλž˜μ„œ 영ν–₯도가 μ–΄λ””κΉŒμ§€ μ „νŒŒλ κΉŒμ΄λ‹€.

// A의 μ‹€ν–‰
viewModelScope.launch {
    // B의 μ‹€ν–‰
    launch {
        coroutineScope {
            // D의 μ‹€ν–‰
            launch { }
            // E의 μ‹€ν–‰
            launch { }
        }
    }
    // C의 μ‹€ν–‰
    launch {
        withContext(Dispatchers.IO) {
            // F의 μ‹€ν–‰
            launch { }
        }
    }
}

이 μ½”λ“œλŠ” μ–΄λ””μ—μ„œ 였λ₯˜κ°€ λ‚˜λ“  viewModelScope.launch {}인 Aκ°€ μ’…λ£Œλœλ‹€.

그렇기에 λ¬Έμ œμ— λ‚˜μ˜¨ 닡변은 정닡이 없을 수 μžˆλ‹€.


κ·Έλž˜λ„ κΆκΈˆν•˜μž–μ•„

그럼 μœ„ μ½”λ“œμ—μ„œ μΌλΆ€μ˜ μž‘μ—…μ„ Job()둜 λ‹€μ‹œ λ³€κ²½ν•˜λ©΄ 영ν–₯은 μ–΄λ–»κ²Œ λ‹¬λΌμ§ˆκΉŒ?

μ•žμ„œ μ μ—ˆλ˜ μ½”λ“œμ— B의 μž‘μ—…μ— Job()으둜 μΉ˜ν™˜ν•˜λ©΄ μ–΄λ–€ 일이 λ²Œμ–΄μ§ˆκΉŒ?

// A의 μ‹€ν–‰
CoroutinesScope(SupervisorJob()).launch {
    // B의 μ‹€ν–‰
    launch(Job()) { // Job으둜 λ³€κ²½
        // D의 μ‹€ν–‰
        launch { throw Exception("exception") }
        // E의 μ‹€ν–‰
        launch { }
    }
    // C의 μ‹€ν–‰
    launch {
        // F의 μ‹€ν–‰
        launch { }
    }
}

κ°„λ‹¨ν•˜λ‹€.

  • AλŠ” ꡬ쑰화가 깨진 B의 영ν–₯을 받지 μ•ŠμœΌλ©°, C λ‚΄λΆ€μ—μ„œ λ°œμƒν•œ 였λ₯˜ μ—­μ‹œ 영ν–₯받지 μ•ŠλŠ”λ‹€.
  • BλŠ” μƒˆλ‘œμš΄ Job을 μƒμ„±ν•˜μ˜€κΈ°μ— A와 λ¬΄κ΄€ν•˜λ©° D/E의 μž‘μ—… μ‹€νŒ¨μ— λŒ€ν•œ 영ν–₯ λ°›λŠ”λ‹€.
  • CλŠ” Fμ—μ„œ λ°œμƒν•˜λŠ” 였λ₯˜μ— 영ν–₯받지 μ•ŠλŠ”λ‹€.
  • CλŠ” μƒˆλ‘œμš΄ Job을 μƒμ„±ν•œ Bμ—μ„œ λ°œμƒν•œ 였λ₯˜μ— λŒ€ν•œ 영ν–₯ 받지 μ•ŠλŠ”λ‹€.

이런 μ΄μœ λŠ” μ œλ―Έλ‚˜μ΄μ—μ„œ μ„€λͺ…ν•œ Job의 μ˜ˆμ™Έ μ„€λͺ…μ—μ„œ μΆ©λΆ„νžˆ 잘 λ‚˜μ˜¨λ‹€.

Job λŒ€μ‹  μ‚¬μš©ν•  수 μžˆλŠ” μ•ˆμ „ν•œ 방법은

supervisorScope을 μ‚¬μš©ν•˜λŠ” 방법이 μžˆμœΌλ‹ˆ μ°Έκ³ ν•˜μ—¬ μ‚¬μš©ν•˜μ‹œκΈΈ

// A의 μ‹€ν–‰
CoroutinesScope(SupervisorJob()).launch {
    // B의 μ‹€ν–‰
    supervisorScope {
        launch { // Job으둜 λ³€κ²½
            // D의 μ‹€ν–‰
            launch { throw Exception("exception") }
            // E의 μ‹€ν–‰
            launch { }
        }
    }
    // C의 μ‹€ν–‰
    launch {
        // F의 μ‹€ν–‰
        launch { }
    }
}


κ°„λ‹¨ν•œ 원리 μ •μ • 포함

λ‚΄λΆ€ μ½”λ“œλž‘ λ¬΄κ΄€ν•˜κ²Œ μ•„λž˜μ™€ κ°™λ‹€.

val a = viewModelScope.launch {
    // μ—¬κΈ°μ„œ 였λ₯˜κ°€ λ‚˜κ±°λ‚˜
}

val b = viewModelScope.launch {
    // μ—¬κΈ°μ„œ 였λ₯˜κ°€ λ‚˜κ±°λ‚˜
}

μœ„μ™€ 같이 a/b의 μ΅œμƒμœ„ viewModelScope에 λŒ€ν•œ launch {}λ₯Ό μ‹€ν–‰ν•  λ•ŒλŠ” a/b 쀑 μ–΄λŠ κ³³μ—μ„œ exception이 λ°œμƒν•˜λ”λΌλ„ μ„œλ‘œ 영ν–₯ 받지 μ•ŠλŠ”λ‹€.

이전에 였λ₯˜λ₯Ό λ²”ν•œ μ΄μœ λŠ” μ•„λž˜μ™€ 같은 λ‚΄λΆ€ μ½”λ“œλ₯Ό μ°Έκ³ ν•˜μ˜€κΈ° λ•Œλ¬Έμ΄λ‹€.

λ‹Ήμ—°ν•˜κ²Œλ„ launch의 μƒμœ„ viewModelScope.launch {}λ₯Ό λΆ€λͺ¨λ‘œ 가지고 μžˆμ—ˆμ„κ±°λΌκ³  μ°©κ°ν•œ 뢀뢄이닀.

private class SupervisorJobImpl(parent: Job?) : JobImpl(parent) {
    override fun childCancelled(cause: Throwable): Boolean = false
}

μ§€κΈˆ ν˜„μž¬μ˜ Jobκ³Ό Parent Job을 λͺ¨λ‘ 가지고 μ†Œν†΅ν•œλ‹€λŠ” 점을 μ•Œ 수 μžˆλ‹€.

private val _parentHandle = atomic<ChildHandle?>(null)
internal var parentHandle: ChildHandle?
    get() = _parentHandle.value
    set(value) { _parentHandle.value = value }

override val parent: Job?
    get() = parentHandle?.parent


마무리

κ°„λ‹¨ν•˜κ²Œ 2025 μ•ˆλ“œλ‘œμ΄λ“œ νƒκ΅¬μ˜μ—­μ˜ Exception μ „νŒŒμ— λŒ€ν•΄ μ •λ¦¬ν•˜μ˜€λ‹€.

이 문제λ₯Ό μ•Œ 수 μžˆλŠ” 것은

  • Job은 cancel/fail λ°œμƒ μ‹œ ν˜„μž¬ μƒνƒœλ₯Ό Cancelled μ²˜λ¦¬ν•œλ‹€.
  • Job은 ν•˜μœ„ context에 μ „λ‹¬λ˜μ§€ μ•Šμ§€λ§Œ Parent.job은 ν•˜μœ„ Jobμ—μ„œλ„ λ³„λ„λ‘œ κ΄€λ¦¬ν•œλ‹€.
  • SupervisorJobκ³Ό Job을 쀑간에 ν˜Όμš©ν•΄μ„œ μ‚¬μš©ν•˜λŠ” κ²½μš°μ—λŠ” Job 뢀뢄은 λͺ¨λ‘ μ’…λ£Œ μ²˜λ¦¬ν•œλ‹€.
  • launch둜 μ‹€ν–‰ν•  경우 return κ²°κ³ΌλŠ” Job이 λœλ‹€.

Jobκ³Ό SupervisorJob의 μ€‘μš”μ„±μ„ 이해할 수 μžˆλŠ” κ°„λ‹¨ν•œ λ¬Έμ œμž…λ‹ˆλ‹€.


μ •μ •ν•˜λ©΄μ„œ

AIμ—κ²Œ μ§ˆλ¬Έμ„ λ‹€ λ‚¨κ²¨λ³΄μ•˜λŠ”λ° μ„œλ‘œ λ‹€λ₯Έ 닡변을 μ€€λ‹€.

AI의 응닡이 100% 닡은 μ•„λ‹ˆκ² μ§€λ§Œ 일뢀 λ§žλ‹€κ³  κ°€μ •ν•˜κ³  μ΄μ•ΌκΈ°ν–ˆκ³ , λ‚΄κ°€ μ•Œκ³  있던 λ‚΄μš©μ„ μ „λ‹¬ν•˜λ©΄μ„œ 였λ₯˜κ°€ 생겼닀. μ λ‹Ήνžˆ μ•Œλœ°ν•˜κ²Œ μ‚¬μš©ν•˜λ©΄ μœ μš©ν•œ λ„κ΅¬μž„μ€ λ§žλ‹€.

κ²°κ΅­ μ½”λ“œ 검증을 톡해 잘λͺ»λ˜μ—ˆλ‹€λŠ” κ±Έ λ°œκ²¬ν•˜κ³  λ‚˜μ„œ μˆ˜μ •μ€ ν–ˆμ§€λ§Œ μž¬λ―ΈμžˆλŠ” λ¬Έμ œμž„μ€ λ§žλ‹€.

μ‹€μ œ κ°œλ°œν• λ•ŒλŠ” μ΄λ ‡κ²Œ μ‚¬μš©ν•˜λ”λΌλ„ runCatchλ₯Ό 항상 달고 μ‚¬μš©ν•˜λ‹ˆ 크게 문제λ₯Ό λŠλΌμ§€ λͺ»ν–ˆμ—ˆλ‹€.


코루틴 μ˜ˆμ™Έ μ „νŒŒμ— λŒ€ν•œ μ˜μƒ μΆ”κ°€

μš°ν…Œμ½” 크루 였λ‘₯μ΄λ‹˜μ˜ 코루틴 μ˜ˆμ™Έ 처리 μ˜μƒμ„ μΆ”κ°€ν•©λ‹ˆλ‹€.



About Taehwan

My name is Taehwan Kwon. I have developed Android for 6 years and blog has been active for eight years.

Comments