본문 바로가기

Android

Coroutines 이것만 알고 사용하자!

개요

코루틴(Coroutine)은 구조화된 동시성을 기반으로 비동기 작업을 효율적으로 처리합니다.

안드로이드 앱은 main thread에서 onDraw()를 지속적으로 호출하며 화면을 그립니다. 그렇기에 네트워크 통신이나 파일에 접근하는 작업들을 main thread에서 실행하면 화면이 버벅이거나 ANR이 발생합니다.

코루틴은 이러한 시간이 오래 걸리는 작업들이 background thread에서 실행되도록 돕습니다.

 

주요 개념

suspend, resume

코루틴은 suspend function을 통해 비동기적인 작업들을 콜백이 아닌 순차적인 코드로 표현할 수 있습니다.

실행 중인 코루틴 내에서 suspend function을 만나면 해당 작업을 위해 다음 모든 작업들이 일시정지(suspend) 되고, 해당 작업이 완료되면 다시 돌아와(resume) 기존 작업들을 실행하기 때문입니다.

 

Dispatchers

코루틴은 Dispatchers를 통해 코루틴이 실행될 thread를 지정합니다.

  • Dispatchers.Main : Android thread로 지속적으로 화면을 그리기에 UI와 관련된 작업들을 실행
  • Dispatchers.IO : 네트워크 호출, 파일 입출력, Database 접근에 관련된 작업들을 실행
  • Dispatchers.Default : Json 파싱과 같은 CPU 사용량이 많은 연산(계산) 작업들을 실행

코루틴 실행 시 별도의 Dispatchers를 지정하지 않으면 Dispatchers.Default가 기본적으로 사용됩니다.

public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {
    val combined = foldCopies(coroutineContext, context, true)
    val debug = if (DEBUG) combined + CoroutineId(COROUTINE_ID.incrementAndGet()) else combined
    return if (combined !== Dispatchers.Default && combined[ContinuationInterceptor] == null)
        debug + Dispatchers.Default else debug
}

 

CoroutineScope

CoroutineScope는 코루틴이 실행되는 범위를 나타내며 코루틴의 lifecycle을 관리합니다. Android에서는 주로 lifecycle이 특정된 클래스인 ViewModel의 viewModelScope, Activity or Fragment의 lifecycleScope를 제공합니다.

 

CoroutineContext

CoroutineContext는 코루틴이 어떻게 실행되고 동작하는지에 대한 정보를 지정 및 관리합니다. 주로 코루틴 실행 시 인자로 전달하며 '+' 연산자를 통해 아래의 구성 요소들을 결합하여 사용합니다.

  • Job: 코루틴의 생명주기 관리 및 코루틴이 실행 중인지, 취소되었는지 추적
  • CoroutineDispatcher: 코루틴이 어느 스레드에서 실행될지 결정
  • CoroutineName: 코루틴의 이름을 지정(디버깅 시 유용)
  • CoroutineExceptionHandler: 예외가 발생 시 처리

 

Coroutines Builders

Coroutines Builders는 코루틴 실행 시 사용합니다.

  • launch : 코루틴 실행하고 결과를 반환하지 않음
  • async : 코루틴 실행하고 Deferred 감싼 결과를 반환하기에 await()를 호출하여 결과에 접근

일반적으로 launch를 사용하며, async는 동시에 여러(병렬로) 비동기 작업 실행 후 (한번에) 결과를 처리해도 괜찮은 경우에 사용합니다. 왜냐하면 launch는 순차적으로 실행하므로 하나가 끝나야 다음 비동기 작업 실행하기에 병렬 작업 시 오래 걸리기 때문입니다.

 

사용하는 방법

1. 직접 Coroutines를 사용하는 경우

별도로 thread 지정하지 않으면 Dispatchers.Default에서 실행

val job = Job()
val ioDispatcher = Dispatchers.IO
val coroutineScope = CoroutineScope(job + ioDispatcher)
coroutineScope.launch {
    delay(2000L)
}

// 비동기 작업 완료 후 직접 scope를 취소해야 합니다.
coroutineScope.cancel()

 

2. Lifecycle과 연결하여 Coroutines를 사용하는 경우

Activity or Fragment, ViewModel에서 사용하는 경우이고,
별도로 thread 지정하지 않으면 Dispatchers.Main에서 실행

 

Activity or Fragment에서,
CoroutineScope는 LifecycleOwner의 Lifecycle과 결합하여 실행하고, Lifecycle이 DESTROYED이면 취소된다.

lifecycleScope.launch { 
    delay(2000L)   
}

 

ViewModel에서,
CoroutineScope는 Activity or Fragment와 연결된 ViewModel과 결합하여, 바로 실행하고, onCleared()이면 취소됩니다.

viewModelScope.launch { 
    delay(2000L)   
}

 

 

 

예제에서 사용한 delay(2000L)는 suspend function으로 Coroutine 내에서 thread를 block하지 않고 지정한 시간동안 지연시키는 기능입니다.

public suspend fun delay(timeMillis: Long) { ... }

참조