본문으로 바로가기

[Android]코틀린 Coroutine

category AndroidStudio 2022. 5. 31. 22:48

1. Coroutine이란?

 

 코루틴은 흔히 경량 스레드라고 불리며, 스레드 위에서 실행되는 하나의 일(Job)이라고 이해할 수 있다.

 

코루틴은 코루틴이 시작된 스레드를 중단하지 않으면서 비동기적으로 실행되는 코드이다.

 코루틴은 스레드 위에서 실행되기 때문에 여러가지 코루틴이 존재할 때, 코루틴1이 실행 중 코루틴2가 실행되어도 실행중인 스레드를 정지하면서 컨텍스트 스위칭 개념으로 다른 스레드로 전환하는 것이 아니라, 기존 스레드를 유지하며 코루틴2를 실행하게 된다.

이 후 코루틴1을 다시 실행할 때 저장해둔 코루틴1의 상태를 불러와 다시 스레드에서 코루틴1을 실행하게 된다. 즉, 스레드의 멈춤 없이 루틴을 돌릴 수 있으며 여러 스레드를 사용하는 것보다 훨씬 적은 자원을 소모하게 된다.


2. Coroutine Scope

  • GlobalScope : 앱의 생명주기와 함게 동작하기 때문에 실행 도중에 별도 생명 주기 관리가 필요없음. 시작 ~ 종료 까지 긴시간 동안 실행되는 코루틴의 경우에 적합 (안드로이드 앱 개발에서는 거의 사용하지 않음)
  • CoroutineScope : 버튼 이벤트로 Download 하거나 서버에서 이미지를 열 때 등, 필요할 때만 열고 완료되면 닫아주는 코루틴 스코프를 사용할 수 있다.
  • ViewModelScope : Jetpack 아키텍처의 뷰모델 컴포넌트 사용 시 ViewModel 인스턴스에서 사용하기 위해 제공되는 스코프. 해당 스코프로 실행되는 코루틴은 ViewModel 인스턴스가 소멸될 때 자동으로 소멸된다.

모든 코루틴은 스코프 내에서 실행되어야 하는데 이를 통해서 액티비티 또는 프래그먼트의 생명주기에 따라 소멸될 때 관련 코루틴을 한번에 취소할 수 있는데 이는 곧 메모리 누수를 방지한다.


 

3. Coroutune Context

코루틴은 항상 Kotlin 표준 라이브러리에 정의된 CoroutineContext 유형의 값으로 표시되는 일부 컨텍스트에서 실행된다.

주요 요소는 코루틴의 JobDispatcher가 있다.

 

코루틴의 Context는 코루틴이 실행 될 Context로, 코루틴의 실행 목적에 맞게 실행 될 스레드 풀을 지정해 준다.

 

Dispetchers 종류

  • Dispatchers.IO
    • 코투틴은 공유된 스레드 풀에 있는 백그라운드 스레드에서 동작한다.
    • 로컬 DB와 작업할 때, 네트워크 작업 또는 파일과 관련된 작업을 할 때 사용한다
  • Dispatchers.Main
    • 코루틴이 메인스레드에서 동작한다.
    • UI관련 함수, suspending 함수, LiveData에서 수정사항을 가져오는 함수 등, 작고 가벼운 작업들만 실행 시키는 것이 좋다.
    • 코루틴을 메인스레드에서 시작하고 백그라운드 스레드로 변경하는 것이 Best Practice이다.
  • Dispatchers.Default
    • 많은 리스트를 정렬하는 작업같이 CPU 부하가 많은 작업을 할 때 유용하다.
CoroutineScope(Dispatchers.IO).launch {
    //TODO something...
}

위의 예시는 CoroutineScope의 Context로서 Dispetchers.IO를 사용한 경우이다.

 

val job = Job()

CoroutineScope(Dispatchers.IO + job).launch {
    //TODO something...
}

만약 Job instance를 CoroutineScope의 Context로서 사용하고 싶은 경우 job instance의 이름을 포함하면 된다. 

'+' 연산자는 다수의 코루틴 컨텍스트를 merge하는데 사용된다. 

 


4. Cotoutine Builder

Coroutine Scope의 확장 함수로서 새로운 코루틴을 실행하기 위해 사용된다.

Coroutine Builder의 종류에는 4가지가 있다.

 

  • Launch
    • 현재 스레드를 blocking 하지 않고 새로운 코루틴을 실행한다.
    • job 인스턴스를 반환하고 그 인스턴스는 코루틴에 대한 참조로 사용된다.
    • 리턴 값이 없는 코루틴을 위해 사용한다.
    • 반환 된 job 인스턴스로 코루틴을 제어 할 수 있다. ex) job.join()
  • Async
    • 현재 스레드를 blocking 하지 않고 새로운 코루틴을 실행한다.
    • Deferred<T>의 인스턴스를 반환한다. 값을 얻기 위해서 await() 함수를 사용한다.
    • Deferred 인터페이스는 job 인터페이스를 확장한 것이다. 
    • 결과 값을 반환하고 싶은 경우에 Async Builder를 사용한다.
  • produce
    • 요소 스트림을 만드는 코루틴을 위한 Builder이다.
    • reveiveChannel 인스턴스를 반환한다.
  • runblocking
    • 다른 Coroutine Builder들과 다르게 작업이 끝날 때 까지 스레드를 Block 한다.
    • T 타입의 결과를 반환한다.
val list = ArrayList<Int>()
CoroutineScope(Dispatchers.Main).launch {
    val launchJob = CoroutineScope(Dispatchers.Main).launch { //_ 결과 값 리턴하지 않고 해당 코루틴을 제어할 수 있는 job을 리턴
        for (i in 0 until 10) {
            list.add(i)
        }
    }

    val asyncJob = CoroutineScope(Dispatchers.Default).async { //_ async 블록의 마지막 값이 Diferred로 Wrapping 되며 Differred를 리턴
        for (i in 10 until 20) {
            list.add(i)
        }
        val value = list
        value //_ == Deferred<ArrayList<Int>>

    }

    launchJob.join() //_ 코루틴이 완료될 때 까지 대기
    Log.d("TEST_LOG", "${list}") //_ 코루틴 완료 된 후 list 출력 / launchJob을 join()을 통해 결과 값을 얻을 때 까지 대기하지 않으면 list는 empty 상태로 로그에 출력
    val resultList = asyncJob.await() //_ 코루틴이 완료될 때 까지 대기 / 별도의 delay가 없더라도 Dispatchers.Default 컨텍스트로 얻어진 스레드 풀로부터 결과 값을 수신할 떄 까지 Main Thread가 일시 중지 된다.
    Log.d("TEST_LOG", "${resultList}") //_ Diferred에서 await() 메소드를 통해 결과값을 수신하여 출력
    Log.d("TEST_LOG", "${list}") //_ 코루틴 완료 된 후 list 출력 / asyncJob을 await()을 통해 결과 값을 얻을 때 까지 대기하지 않으면 list의 결과값은 업데이트 되지 않은채 로그에 출력됨.
}

launch Builder로 반환받은 job 인스턴스는 join() 함수를 통해 코루틴이 완료될 때 까지 기다릴 수 있으며, async Builder로 반환받은 

deferred 인스턴스는 await() 함수를 통해 코루틴이 끝날 때 까지 결과를 기다릴 수 있다.