본문 바로가기
Android/Kotlin

Kotlin - Coroutine 추가 정리 3

by 2Plus 2020. 5. 31.

[이전 글]

2020/05/27 - [Android/Kotlin] - Kotlin - Coroutine 추가 정리 1

2020/05/29 - [Android/Kotlin] - Kotlin - Coroutine 추가 정리 2

 

 

Callback 대신 Coroutine 쓰기

[Callback]

 

 

 

 Callback을 사용한 간단한 예시이다. Network Request를 하면 fetch를 하고, fetch가 완료되면 callback이 불리는 구조이다. callback은 받은 결과를 화면에 표시해준다. 이렇게 callback을 사용하면 fetch를 부르고 결과를 기다리거나 하지 않고 이와 상관 없이 networkRequest() 함수는 종료되게 된다. networkRequest() 함수 내에서 fetch 결과에 따라 순차적으로 무언가를 진행하려면 callback block이 더 길어지거나 추가적인 코드들이 더 필요하게 된다.

 

[Coroutine]

 

 

 

 

 앞의 callback 예시와 동일하게 동작하는 Coroutine 코드이다. 이렇게 Coroutine으로 표현하면 depth도 줄어들고 더 sequential 해진다. 콜백이 계속 중첩되는 경우에도 복잡하지 않게 사용할 수 있는 장점이 있다.

 

 

Coroutine Context

 Coroutine은 Coroutine Context에서 실행된다. Kotlin Standard Library에는 CoroutineContext가 정의되어 있는데, Job과 Dispatcher를 가질 수 있는 Set이다. 

[Job]
 Coroutine Scope는 Job을 통해 Coroutine의 Life time을 조절한다. 어떠한 Coroutine Scope의 Job을 cancel하게 되면, 해당 스코프 내에서 동작하고 있던 모든 Coroutine이 취소된다. 이러한 동작은 유저가 현재 화면에서 완전히 다른 화면으로 넘어가게 되어, 더 이상 앞에서 진행하고 있던 작업들이 필요 없어진 경우에 한 번에 취소하기 편리하다.

[Dispatcher]
 Coroutine Scope는 기본 Dispatcher를 지정해줄 수도 있다. Dispatcher는 Coroutine이 어떤 쓰레드에서 수행되는지를 조절할 수 있다. Coroutine이 하나의 thread에서 실행되도록 할 수도 있고, thread pool에서 실행되도록 할 수도 있고, 따로 정의하지 않은 채로 실행할 수도 있다.

 사실 앞의 예시들에서 계속 사용해왔던 Coroutine Builder는 CoroutineContext를 파라미터로 받고 있다. 그런데 따로 표기하지 않아도 잘 동작했던 이유는 optional로 받기 때문이다. 직접 지정한 CoroutineContext는 Coroutine Builder를 호출할 때 지정할 수도 있고, CoroutineScope에 정의하면 Child Coroutine에서 해당 Context를 자동으로 받아 사용할 수 있다.

 

 

Dispatchers

 

 

 

 CoroutineContext에 Default Dispatcher를 적용한 예시이다.

 

 쓰레드를 지정하기 위한 Dispatcher는 다음과 같은 것들을 사용할 수 있다.

  1. Dispatchers.Default
  2. Dispatchers.IO
  3. Dispatchers.Main
  4. Dispatchers.Unconfined
  5. newSingleThreadContext

 Dispatchers.Default는 예시에서 계속 사용했던 GlobalScope의 기본 Dispatcher이다. 이것은 Shared Background Thread Pool 을 사용한다. 다음 두 가지 launch가 동일한 dispatcher를 사용하는 것이다.

 

 

 Dispatchers.IO는 입출력용 쓰레드에서 동작하게 된다.

 

 newSingleThreadContext는 새로운 thread를 생성할 때 사용하면 된다.

 

 Dispatchers.Unconfined는 caller thread에서 Coroutine이 동작하는데, suspend function이 일시 중단되었다가 다시 진행될 때에는 suspend function을 재개한 쓰레드에서 동작한다. 이는 UI를 업데이트 하지 않는 경우처럼 특정 쓰레드에서 동작해야 하는 작업이 아닌 경우에만 사용하기에 적절하다. 일반적인 코드에서는 사용하지 않는 것을 추천한다.

 

 

Job

 Coroutine의 Job은 위와 같은 6 가지의 상태가 존재한다. 이 Job을 바탕으로 Coroutine의 현재 진행 상태를 파악하고 제어할 수 있다. job.cancel()을 사용하여 작업을 취소할 수도 있다.


 참고로 CoroutineContext에 Job을 전달해줄 수도 있지만, Coroutine Builder인 launch의 반환형을 살펴보면 Job을 반환하고 있다.
 Job을 생성하여 전달해주거나 반환 받은 Job을 통해 할 수 있는 것은 다음과 같다.

 

  1. start()
  2. join()
  3. cancel()
  4. cancelAndJoin()
  5. cancelChildren()

 

 각각을 살펴보면 다음과 같다.


 start()는 Coroutine 상태를 확인하여 동작 중이면 true, 준비/완료 상태이면 false를 반환한다.


 join()은 Coroutine 동작이 끝날 때까지 실행 중인 코드를 잠시 대기시킨다. await()처럼 사용할 수 있는 것이다.


 cancel()은 Coroutine을 즉시 종료하라고 신호를 보낸다. 하지만 작업이 계속 진행되는 Coroutine의 경우 종료되지 않는 경우도 있을 수 있다.


 cancelAndJoin()은 Coroutine을 즉시 종료하라는 신호를 보내고, 정상 종료가 될 때까지 대기까지 한다.


 cancelChildren()은 CoroutineScope 내의 Children Coroutine들을 취소한다. cancel()과는 달리 Child Coroutine들만 취소되고 Parent는 종료되지 않는다.

 

 

 

 어떤 ViewModel 안에서 네트워크 요청이나 DB 읽기/쓰기 등 시간이 걸리는 작업을 수행할 때 Coroutine을 사용한다고 생각해보면. 위와 같이 사용할 수 있다.

 

 

GlobalScope

 GlobalScope는 Singleton Object로, CoroutineContext로써 EmptyCoroutineContext를 가지고 있다. EmptyCoroutineContext는 구현해야 하는 CoroutineContext 멤버들에 대해서 기본적인 것만 구현한 컨텍스트이다. 여기에 Job은 따로 정의되어 있지 않기 때문에 앱 프로세스와 생명 주기를 함께 한다. 그렇기 때문에 GlobalScope.launch{ } 로 진행되는 Coroutine 작업은 앱이 종료될 때까지 계속 실행되기 때문에 사용에 있어 약간의 주의가 필요하다.

 


 이 글에 있는 것들은 Coroutine 중에 일부만 있는 것이며 더 다양하고 많은 기능들이 제공된다. 그때그때 필요한 것을 찾아보면서 사용하면 좋을 것 같다. Exception, channel, timeout 등등 더 많은 기능이 있다.

 

 

반응형

댓글