Android 개발/Kotlin

Kotlin Coroutine에 대한 정리

Developer88 2019. 10. 9. 02:58
반응형

이제 안드로이드 개발을 하면서, RxJava나 AsyncTask이외에도

다른 Thread로 작업을 할 수 있는 방법이 생겼는데요.

바로 Kotlin의 Coroutine입니다.

Coroutine은 RxJava만큼 다양한 Operator를 제공해주지는 않지만,

심플하다는 면에서는 좀 더 좋은면이 있는 것 같습니다.

오늘은 이 Coroutine에 대해서 정리해 보도록 하겠습니다.

 

1. 라이브러리 implement

가장 먼저 해야할 것은 역시 Library설정인데요.

Kotlin의 버전을 최신 버전으로 하구요.

아래와 같이 app레벨의 build.gradle에서 implement해 줍니다.

 

마치 rxjava와 rxandroid를 같이 implement했듯이,

coroutine도 core와 android 라이브러리를 같이 implement해 줍니다.

 

 

2. Coroutine

Coroutine은 Thread안에 여러개로 존재할 수 있는 작업 아이템인데요.

라이트한 Thread와 같다고 표현하기도 합니다.

 

아래 보시면 Coroutine의 delay메소드가 있는데요.

Coroutine의 delay는 해당 Thread에 대한 것이 아니라,

Thread안의 Coroutine에 대한 delay 입니다.

 

 

Thread.sleep()이 해당 쓰레드에 대해 delay를 시키는 것인데 반해,

Coroutine의 delay는 Thread안에 있는 자신에 대한 delay만 발생시킵니다.

Coroutine과 Thread가 다른 개념이라는 것을 알고 있어야 Coroutine의 다른 함수들을

올바르게 사용할 수 있습니다.

 

 

3. 구현

Coroutine을 정의하고 시작하는 방법등에 대해서 정리해 보도록 하겠습니다.

가장 먼저 할 일은 Coroutine들이 작업될 수 있는 Scope를 정하는 것 인데요.

Scope를 정한후에는 launch또는 async메소드를 이용해 작업을 시작해 줄 수 있습니다.

먼저 Scope를 정의하는 것 부터 보도록 하겠습니다.

3-1. Coroutine Scope

위에서 Coroutine이 특정 Thread에서 할 수 있는 작업들 이라고 하였는데요.

이 작업들의 범위를 정한다는 것은, 어떤 Thread에서 할 것이냐를 정의하는 것 이기도 합니다.

밑에서 선택할 수 있는 상수값들을 보도록 하겠습니다.

 

 

CoroutineScope에는 IO, Main, Default 중 하나를 인자로 넣어서 Coroutine코드들의 scope를 정의할 수 있는데요.

이들은 Coroutine을 suspend하거나 resume할 수 있는 dispatcher들로,

어떤 Thread에서 어떻게 동작할지에 대한 정보를 담고 있습니다.

 

각각의 용도는 다음과 같은데요. 이를 참조하여, scope메소드에 인자로 넣어주면 됩니다.

  • Dispatchers.IO -  disk 또는 network I/O 에 최적화 되어 워커쓰레드에서 돌아갑니다.
  • Dispatchers.Default -  CPU 연산이 많이 필요한 긴 JSON파싱 혹은 긴 List Sorting등에 최적화 되어 있습니다. 
  • Dispatchers.Main - 너무나 잘 알고 계시겠지만 UI와 상호작용을 필요로 하거나 정말 간단한 작업등에 사용할 수 있습니다.

CoroutineScope는 Coroutine을 시작하는 곳이기도 하지만,

모든 Coroutine을 관리하는 곳이기도 하구요.

Coroutine을 끝내는 곳이기도 합니다.

어떤 비동기 코드도 Coroutine이 끝나면 같이 끝나버립니다.

 

이런점이 코딩을 할 때, 주의를 기울여야 하는 부분이기도 하지만,

안드로이드 같이 lifeCycle에 따라 리소스에 대한 release가 중요한 프레임워크에서는

매우 편리한 부분이기도 합니다.

Scope가 끝나면 모든 작업이 끝난다는 것을 보장해주기 때문이지요.

 

3-2. Supspend & Resume

Coroutine을 사용하면서 Suspend와 Resume에 대한 개념을 잠시 알고 갈 필요가 있는데요.

Suspend와 Resume은 콜백방식을 대체한 Coroutine만의 메카니즘으로 각각 다음과 같은 의미가 있습니다.

  • Suspend - 모든 Local변수를 저장하고, 현 Scope에서의 Coroutine의 실행을 pause 합니다.
  • Resume - pause됬던 곳에서, Coroutine을 계속 실행합니다.

Suspend와 Resume 기능은,

Kotlin의 function에 suspend라는 키워드를 붙이거나,

launch나  async처럼 코틀린을 시작하는 함수를 사용함으로서 더해지게 됩니다.

 

Coroutine의 스코프에서 사용되어 지는 함수는 suspend라는 키워드를 아래와 같이 붙여 주어야 합니다.

 

 

 

3-2. Coroutine을 시작하는 방법( launch & async)

위의 코드들에서 launch함수를 보셨을 텐데요.

Coroutine을 시작하는 방법에는 launch와 async 함수를 사용하는 두가지 방법이 있습니다.

  • launch - 새로운 Coroutine을 시작하지만, 결과값을 전달하지 않을 때 사용합니다. 
  • async - result를 return할 수 있는 Coroutine을 시작하는 경우 이 메소드를 사용해 줍니다.

둘의 가장 큰 차이는 return값이 있는냐 이구요.

어느 것을 사용하느냐에 따라서 사용하는 메소드나 완료 대기에 대한 대응이 조금씩 달라집니다.

 

3-3. Job과 Deferred(연기 혹은 완료대기)

A. launch함수의 Job

launch함수로 시작한 Coroutine은 Job타입의 객체를 return합니다.

아래와 같이 return된 job에 join()함수를 이용해서,

코드가 완료가 될 때 까지 기다릴 수 있는데요.

Job이 여러개인 경우에는, joinAll()함수를 이용해 모든 Coroutine이 완료되는 것을 기다릴 수도 있습니다.

 

 

B. async함수의 Deferred

async는 return할 결과 값이 있는데요, Deferred<T> 타입으로 객체를 return해 줍니다.

async함수는 await()함수로 완료가 되기를 기다릴 수 있는데요.

특히, suspend키워드로 시작하는 Coroutine은 해당 함수가 return하는 시점에는 작업이 멈춰있어야 하므로,

await()나 awaitAll()을 이용해서 return전에 결과값을 가지도록 해 주어야 합니다.

(return전에 결과값이 않나온다면, Exception이 발생할 수 밖에 없겠지요.)

 

 

async는 await는 return할 값이 있으므로,

launch함수의 join()을 사용할 때보다 return이나 exception처리에 있어서 주의가 필요합니다.

 

- await가 호출되면, await호출시에 exception을 들고있다가 다시 rethrow해 주는데요.

await호출시 새로운 coroutine을 실행시키면, exception이 drop되어 버릴수가 있습니다.

- 여러개의 async가 존재하는 경우, 첫번째 async의 return값만을 return해 줍니다.

 

c. runBlocking

runBlocking을 이용해서 Block내의 코드가 실행 완료될때 까지 Blocking을 해서 기다리게 할 수 있습니다.

단순히 join같은 메소드와 같은 기능이 아니라,

현재 thread를 block하고 실행되는 코드이므로,

main쓰레드에서 이용하는 것은 좋은 방법이 아닙니다.

 

그리고, runBlocking에 관한 공식문서를 보면, 중요한 부분이 있습니다.

이 함수는 coroutine으로부터 사용되어서는 않된다는 것 입니다.

어려운 이야기지만, 일반적인 Blocking코드와 suspend스타일로 적힌 라이브러리들을

bridge해줄 목적으로 설계된 함수여서,

main functions나 테스트 들에서 사용되어져야 한다고 합니다.

 

 

3-4. Lazy한 Coroutine 실행 방법

Coroutine은 launch나 async메소드가 실행되면 바로 시작되는데요.

이를 지연해서 실행시킬 수 있습니다.

launch나  async함수에 인자로 "start = CoroutineStart.LAZY" 를 아래와 같이 넣어주면 됩니다.

- return값이 없는 launch의 경우  start()를 실행해 주거나, join()을 실행해주는 순간 실행되게 되므로,

둘을 실행하는데 큰 차이는 없는데요.

- async의 경우는 start()를 실행하는 경우나 await() 실행되는 경우 모두 시작은 되지만,

start()는 결과값을 return해 주지 않으며, await()와는 다르게 Coroutine코드가 완료될 때까지 기다리지 않습니다.

 

 

3-5. 작업의 취소

a. cancel

기본적인 취소는 launch나 async메소드로 시작하여 얻은 job이나 deferred객체에,

cancel()함수를 사용해 주면 됩니다.

 

 

 

위의코드를 빌드해보면, cancel과 join이 잘 동작하는 것을 볼 수 있습니다.

(이 두 동작을 한번에 할 수 있는 cancelAndJoin()이라는 함수도 있습니다.)

 

 

하지만 cancel()이 잘 동작하지 않는 경우가 있는데요.

복잡한 computation을 하고 있을 경우, cancel에 대해 체크하지 않으면,

cancel()함수를 호출 해도, cancel이 되지 않습니다.

 

따라서, 이럴경우 isActive 프로퍼티를 이용해 cancel에 대해 확인하거나,

(잠시 다른 Coroutine에게 양보하거나 잠시 아무것도 하지 않는)

yield()라는 함수를 호출해서 cancel()에 대해서 확인해야 합니다.

 

 

B. withTimeoutOrNull
만약 네트워크 호출을 하였는데 서버의 상태가 좋지 못하다거나,

디스크 입출력을 하다가 에러가 났다면,

해당 작업을 취소하고 유저에게 알려주는 등의 작업을 해야하는데요.

 

이러한 이유로,

실제 프로덕트에서 중요한 부분은 Timeout과 관련된 부분일 것 같습니다.

Coroutine은 이럴 때 사용하기 쉬운 함수를 제공해 주고 있습니다.

withTimeoutOrNull입니다.

인자에 시간을 넣어주고 메소드를 실행시켜 줍니다.

해당 시간이 지나도 정상적으로 종료되지 않으면,

null을 반환하므로, 이 null값을 이용해서 유저에게 정보를 알려줄 수 있습니다.

 

 

C. Scope와 관련된 주의 사항

Scope객체는 모든 coroutine에 대해서 관리하고 있기 때문에,

한 Scope가 끝나면 해당 Scope에서의 모든 동작이 다 cancel되어 버립니다.

그것이 비동기 동작이어서 언제 끝날지 모르는 작업이라 할지라도,

Scope가 끝나면 다 끝입니다.

 

다른 라이브러리가 Coroutine을 실행시켰고, 그것이 비동기로 돌아가고 있더라도,

그 라이브러리가 존재하는 Scope가 끝나버리면 그 안의 Coroutine작업도 다 cancel되어 버리게 됩니다.

 

Coroutine은 Scope로 시작하고 Scope단위로 끝나버리므로,

항상 Scope블록을 관리하는데 주의를 기울여야 겠네요.

 

 

3-5. 작업할 쓰레드를 변경하는 방법

withContext메소드를 이용하면, Coroutine의 context를 변경할 수 있습니다.

context는 Job이나 위에서 scope를 정할때 사용했었던, Dispatcher들에 대한 정보들을 담고 있습니다.

withContext(Main)메소드를 이용하면, Dispatcher를 변경해서 코틀린 코드가 실행되는 쓰레드를 변경할 수 있는데요.

 

안드로이드에서 Coroutine을 사용한다고 가정해보면,

보통은 네트워크 리퀘스트를 워커 쓰레드에서 시작해서,

이것을 View로 보여주기 위해서, Main쓰레드로 전달해 주게 될 것입니다.

이 메소드 하나만으로 아래와 같이 쉽게 다른 쓰레드(메인쓰레드)에서,

Coroutine코드를 실행할 수 있습니다.

 

아래는 test1과 test2두개를 만들어 IO쓰레드에서 작업 후,

순차적으로 Main쓰레드에서 보여주기 위해서는 아래와 같이 해주면 됩니다.

 

 

대신 한가지 주의할 것은 Main, IO, Default dispatcher들이 suspend나 resume을 할 때,

suspend한 시점의 thread에서 정확하게 resume하지 않을 수 있으므로,

thread안에서만 유효한 변수의 사용을 할 때는 주의를 기울여야 합니다.

 

3-6. Android Architecture 컴포넌트와 함께 사용

Android Architecture 컴포넌트와 함께 사용하면,

라이프 사이클 관리 등 장점도 많이 있지만 주의해야 할 점도 있는데요.

 

Android의 특징을 고려하여서 Coroutine코드는,

ViewModel에 놓는 것이 적합합니다.

View가 시작해서 onDestroy호출 때 까지 살아있다가,

onDestory에서 onClear가 호출되는 조건도 좋구요.

 

ViewModelScope에 대한 것은 다른 글에서 자세히 다루도록 하겠습니다.

 

4. 정리

Coroutine도 하나의 툴일뿐이므로, 어떻게 사용하는 것에 대한 제한은 없지만,

Network requests, JSON 파싱, database의 IO, 또는 큰 사이즈의 list를 iterate하는 곳 등에서 

RxJava등을 대신해 Coroutine이 사용될 수 있을 것 같은데요.

 

심플해서 사용하기 쉬운 편이지만, Scope단위에 따른 관리가 필요하고,

cancel()에 있어서도 신경을 써야할 부분들도 있으며,

여러가지 주의해야 할 부분들이 역시 존재하므로,

계속 사용해 보면서 잘 활용하도록 노력해 봐야겠습니다.

 

728x90