[Coroutine] ์ฝ”๋ฃจํ‹ด ์Šค์ฝ”ํ”„ ๋งŒ๋“ค๊ธฐ

 ๐Ÿ’ก ์•ˆ๋“œ๋กœ์ด๋“œ์™€ ๋ฐฑ์—”๋“œ์—์„œ ์ฝ”๋ฃจํ‹ด ์Šค์ฝ”ํ”„๋ฅผ ์–ด๋–ป๊ฒŒ ๋งŒ๋“ค๊ณ  ํ™œ์šฉํ•˜๋Š”์ง€ ํ•™์Šตํ•˜์˜€์Šต๋‹ˆ๋‹ค.

CoroutineScope ํŒฉํ† ๋ฆฌ ํ•จ์ˆ˜

  • CoroutineScope๋Š” coroutineContext๋ฅผ ์œ ์ผํ•œ ํ”„๋กœํผํ‹ฐ๋กœ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค.
interface CoroutineScope {
    val coroutineContext: CoroutineContext
}

CoroutineScope ๊ฐ์ฒด ์ƒ์„ฑ

  • CoroutineScope ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•œ ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค๊ณ  ๋‚ด๋ถ€์—์„œ ์ฝ”๋ฃจํ‹ด ๋นŒ๋”๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ์ด ๋ฐฉ๋ฒ•์€ ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.
    • cancel์ด๋‚˜ ensureActivce ๊ฐ™์€ ๋ฉ”์„œ๋“œ๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜๋ฉด ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•จ
    • ๊ฐ‘์ž๊ธฐ ์ „์ฒด ์Šค์ฝ”ํ”„๋ฅผ ์ทจ์†Œํ•˜๋ฉด ์ฝ”๋ฃจํ‹ด์ด ๋” ์ด์ƒ ์‹œ์ž‘๋  ์ˆ˜ ์—†์Œ
class SomeClass : CoroutineScope 
  • ๋Œ€์‹  ์ฝ”๋ฃจํ‹ด ์Šค์ฝ”ํ”„ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ํ”„๋กœํผํ‹ฐ๋กœ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค๊ฐ€ ์ฝ”๋ฃจํ‹ด ๋นŒ๋”๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์„ ํ˜ธ๋ฉ๋‹ˆ๋‹ค.
class SomeClass {
    val scope: CoroutineScope = ...
}
  • ๊ฐ€์žฅ ์ข‹์€ ๋ฐฉ๋ฒ•์€ CoroutineScope ํŒฉํ† ๋ฆฌ ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.
    • ์ด ํ•จ์ˆ˜๋Š” ์ปจํ…์ŠคํŠธ๋ฅผ ๋„˜๊ฒจ ๋ฐ›์•„ ์Šค์ฝ”ํ”„๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
    • ์žก์ด ์ปจํ…์ŠคํŠธ์— ์—†์œผ๋ฉด ๊ตฌ์กฐํ™”๋œ ๋™์‹œ์„ฑ์„ ์œ„ํ•ด Job์„ ์ถ”๊ฐ€ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.
public fun CoroutineScope(
    context: CoroutineContext
): CoroutineScope =
    ContextScope(
        if (context[Job] != null) context
        else context + Job()
    )

internal class ContextScope(
    context: CoroutineContext
): CoroutineScope
  • ์ƒ์„ฑ์ž์ฒ˜๋Ÿผ ๋ณด์ด๋Š” ํ•จ์ˆ˜๋Š” ๊ฐ€์งœ ์ƒ์„ฑ์ž๋กœ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.
    • ์ƒ์„ฑ์ž ๋Œ€์‹  ํŒฉํ† ๋ฆฌ ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋ผ

์•ˆ๋“œ๋กœ์ด๋“œ์—์„œ ์Šค์ฝ”ํ”„ ๋งŒ๋“ค๊ธฐ

  • ๋Œ€๋ถ€๋ถ„์˜ ์•ˆ๋“œ๋กœ์ด๋“œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ MVC ๋ชจ๋ธ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•œ MVVM, MVP ์•„ํ‚คํ…์ฒ˜๊ฐ€ ์‚ฌ์šฉ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์ด๋Ÿฌํ•œ ์•„ํ‚คํ…์ฒ˜์—์„œ๋Š” ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ณด์—ฌ ์ฃผ๋Š” ๋ถ€๋ถ„์„ ViewModels๋‚˜ Presenters์™€ ๊ฐ™์€ ๊ฐ์ฒด๋กœ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค.
    • ์ผ๋ฐ˜์ ์œผ๋กœ ์ฝ”๋ฃจํ‹ด์ด ๊ฐ€์žฅ ๋จผ์ € ์‹œ์ž‘๋˜๋Š” ๊ฐ์ฒด์ด๋ฉฐ, ์œ ์ฆˆ ์ผ€์ด์Šค๋‚˜ ์ €์žฅ์†Œ์™€ ๊ฐ™์€ ๊ณ„์ธต์—์„œ๋Š” ์ค‘๋‹จ ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
    • ํ”„๋ž˜๊ทธ๋จผํŠธ๋‚˜ ์•กํ‹ฐ๋น„ํ‹ฐ์—์„œ๋„ ์‹œ์ž‘ํ• ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์•ˆ๋“œ๋กœ์ด๋“œ์—์„œ ์–ด๋–ค ๋ถ€๋ถ„์—์„œ ์ฝ”๋ฃจํ‹ด์„ ์‹œ์ž‘ํ•˜๋“ ์ง€ ๊ฐ„์— ์ฝ”๋ฃจํ‹ด์„ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•์€ ๋ชจ๋‘ ๋น„์Šทํ•ฉ๋‹ˆ๋‹ค.
abstract class BaseViewModel : ViewModel() {
    protected val scope = CoroutineScope(TODO())
}

class MainViewModel() : BaseViewModel {
    fun onCrate() {
        scope.launch {
            // in CoroutineScope
        }
    }
}

์Šค์ฝ”ํ”„์—์„œ ์ปจํ…์ŠคํŠธ ์ •์˜ํ•˜๊ธฐ

  • ์•ˆ๋“œ๋กœ์ด๋“œ์—์„œ๋Š” ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๊ฐ€ ๋งŽ์€ ์ˆ˜์˜ ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•ด์•ผ ํ•˜๋ฏ€๋กœ ๊ธฐ๋ณธ ๋””์ŠคํŒจ์ฒ˜๋ฅผ Dispatchers.Main์œผ๋กœ ์ง€์ •ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.
abstract class BaseViewModel: ViewModel() {
    protected val scope =
        CoroutineScope(Dispatchers.Main + Job())
    override fun onCleared() {
        scope.cancel()
    }
}
  • ์ผ๋ฐ˜์ ์œผ๋กœ ์‚ฌ์šฉ์ž๊ฐ€ ์Šคํฌ๋ฆฐ์„ ๋‚˜๊ฐ€๋ฉด, ์ง„ํ–‰ ์ค‘์ธ ๋ชจ๋“  ์ž‘์—…์„ ์ทจ์†Œํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • ๋”ฐ๋ผ์„œ ์œ„์ฒ˜๋Ÿผ onCleared()๋ฅผ ํ™œ์šฉํ•ด ์Šค์ฝ”ํ”„๋ฅผ ์ทจ์†Œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๋˜ํ•œ ์ž์‹ ์ฝ”๋ฃจํ‹ด๋งŒ์„ ์ทจ์†Œํ•˜๋Š” ๊ฒƒ์ด ๋” ์ข‹์€ ๋ฐฉ๋ฒ•์ด๋ฉฐ, ์ž์‹ ์ฝ”๋ฃจํ‹ด๋งŒ ์ทจ์†Œํ•˜๋ฉด ๋ทฐ ๋ชจ๋ธ์ด ์•กํ‹ฐ๋ธŒํ•œ ์ƒํƒœ๋กœ ์œ ์ง€๋˜๋Š” ํ•œ, ๊ฐ™์€ ์Šค์ฝ”ํ”„์—์„œ ์ƒˆ๋กœ์šด ์ฝ”๋ฃจํ‹ด์„ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
override fun onCleared() {
    scope.coroutineContext.cancelChildren()
}
  • ๋˜ํ•œ ์ฝ”๋ฃจํ‹ด์„ ๋…๋ฆฝ์ ์œผ๋กœ ์ž‘๋™์‹œํ‚ค๊ธฐ ์œ„ํ•ด์„œ SupervisorJob์„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    • ์ฝ”๋ฃจํ‹ด ํ•˜๋‚˜๊ฐ€ ์ทจ์†Œ๋œ ๊ฒฝ์šฐ ๋ถ€๋ชจ์™€ ๋‹ค๋ฅธ ์ž์‹ ์ฝ”๋ฃจํ‹ด์ด ํ•จ๊ป˜ ์ทจ์†Œ๋˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
protected val scope =
    CoroutineScope(Dispatchers.Main + SupervisorJob())

์žกํžˆ์ง€ ์•Š์€ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ

  • ์•ˆ๋“œ๋กœ์ด๋“œ์—์„œ ์•„๋ž˜์™€ ๊ฐ™์€ ์—๋Ÿฌ๋ฅผ ๋„์šธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    • HTTP ํ˜ธ์ถœ๋กœ 401 ์—๋Ÿฌ๋ฅผ ๋ฐ›์œผ๋ฉด ๋กœ๊ทธ์ธ ์ฐฝ์„ ๋„์›Œ์•ผ ํ•ฉ๋‹ˆ๋‹ค.
    • 503 Service Unavailable์˜ ๊ฒฝ์šฐ ์„œ๋ฒ„์— ๋ฌธ์ œ๊ฐ€ ์ƒ๊ฒผ๋‹ค๋Š” ๋ฉ”์„ธ์ง€๋ฅผ ๋ณด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • BaseActivity์— ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ํ•œ ๋ฒˆ๋งŒ ์ •์˜ํ•ด๋‘๊ณ  ๋ทฐ ๋ชจ๋ธ์— ์ „๋‹ฌํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๊ฐ€์žฅ ๋งŽ์ด ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • ์žกํžˆ์ง€ ์•Š์€ ์˜ˆ์™ธ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ CoroutineExceptionHandler๋ฅผ ์‚ฌ์šฉํ•ด ํ•ด๋‹น ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
abstract class BaseViewModel(
    private val onError: (Throwable) -> Unit
): ViewModel() {
    private val exceptionHandler =
        CorouitneEcceptionHandler { _, throwable ->
            onError(throwable)
        }

    private val context =
        Dispathcers.Main + SupervisorJob() + exceptinoHandler

    protected val scope = CoroutineScope(context)
}

viewModelScope์™€ lifecycleScope

  • ์ตœ๊ทผ์—๋Š” ์•ˆ๋“œ๋กœ์ด๋“œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์Šค์ฝ”ํ”„๋ฅผ ๋”ฐ๋กœ ์ •์˜ํ•˜๋Š” ๋Œ€์‹ ์— viewModelScope์™€ lifecycleScope๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • Dispatchers.Main๊ณผ SupervisorJob์„ ์‚ฌ์šฉํ•˜๊ณ , ๋ทฐ ๋ชจ๋ธ์ด๋‚˜ ๋ผ์ดํ”„์‚ฌ์ดํด์ด ์ข…๋ฃŒ๋˜์—ˆ์„ ๋•Œ ์žก์„ ์ทจ์†Œ์‹œํ‚จ๋‹ค๋Š” ์ ์—์„œ ์œ„ ์ฝ”๋“œ์™€ ๋งค์šฐ ๋™์ผํ•˜๋‹ค๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
viewModelScope.launch {
    // on coroutine
}

๋ฐฑ์—”๋“œ์—์„œ ์ฝ”๋ฃจํ‹ด ๋งŒ๋“ค๊ธฐ

  • ๋ฐฑ์—”๋“œ ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ ์ค‘๋‹จ ํ•จ์ˆ˜๋ฅผ ๊ธฐ๋ณธ์ ์œผ๋กœ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.
  • Ktor์—์„œ ๋ชจ๋“  ํ•ธ๋“ค๋Ÿฌ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ์ค‘๋‹จ ํ•จ์ˆ˜๋ฅผ ํ™œ์šฉํ•˜๋ฉฐ, ๋”ฐ๋กœ ์Šค์ฝ”ํ”„๋ฅผ ๋งŒ๋“ค ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.
suspend fun delay(timeMillis: Long) {...}
suspend fun someNetworkCallReturningValue(): SomeType {
 ...
}

์ถ”๊ฐ€์ ์ธ ํ˜ธ์ถœ์„ ์œ„ํ•œ ์Šค์ฝ”ํ”„ ์ƒ์„ฑ

  • ์ถ”๊ฐ€์ ์ธ ์—ฐ์‚ฐ์„ ์‹œ์ž‘ํ•˜๊ธฐ ์œ„ํ•œ ์Šค์ฝ”ํ”„๋ฅผ ์ข…์ข… ๋งŒ๋“ค๊ณค ํ•ฉ๋‹ˆ๋‹ค.
  • ์ด๋Ÿฐ ์Šค์ฝ”ํ”„๋Š” ํ•จ์ˆ˜๋‚˜ ์ƒ์„ฑ์ž์˜ ์ธ์ž๋ฅผ ํ†ตํ•ด ์ฃผ์ž…๋˜๋ฉฐ, ์Šค์ฝ”ํ”„๋ฅผ ํ˜ธ์ถœ ์ค‘๋‹จํ•˜๊ธฐ ์œ„ํ•œ ๋ชฉ์ ์œผ๋กœ๋งŒ ์‚ฌ์šฉํ•˜๋ ค๋Š” ๊ฒฝ์šฐ SupervisorScope๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ๋งŒ์œผ๋กœ ์ถฉ๋ถ„ํ•ฉ๋‹ˆ๋‹ค.
val analyticsScope = CoroutineScope(SupervisorJob())
  • CoroutineExceptionHandler๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์˜ˆ์™ธ๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
private val exceptionHandler =
    CoroutineExceptionHandler { _, throwable ->
        FirbaseCrashlytics.getInstance()
            .recordException(throwable)
    }
val analyticsScope = CoroutineScope(
    SupervisorJob() + exceptionHandler
)
  • ๋˜ํ•œ ๋‹ค๋ฅธ ๋””์ŠคํŒจ์ฒ˜๋ฅผ ์„ค์ •ํ•จ์œผ๋กœ์จ ์ปค์Šคํ…€ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์Šค์ฝ”ํ”„์—์„œ ๋ธ”๋กœํ‚น์„ ํ˜ธ์ถœํ•œ๋‹ค๋ฉด Dispathcers.IO๋ฅผ ์‚ฌ์šฉํ•˜๊ณ , ์•ˆ๋“œ๋กœ์ด๋“œ ๋ฉ”์ธ ๋ทฐ๋ฅผ ๋‹ค๋ค„์•ผ ํ•œ๋‹ค๋ฉด Dispathcers.Main์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
val analyticsScope = CoroutineScope(
    SupervisorJob() + Dispathcers.IO
)