소개

2장_코루틴 인 액션

안드로이드의 UI 스레드

CallFromWrongThreadException

안드로이드는 뷰 계층을 생성하지 않은 스레드가 관련 뷰를 업데이트하려고 할때 를 발생 시킨다
UI 스레드만이 뷰 계층을 생성할 수 있는 스레드이며 뷰를 업데이트 할 수 있다.

NetworkOnMainThreadException

자바에서의 네트워크 동작은 기본적으로 블로킹된다.
UI 스레드에서 네트워크 작업을 수행할 때마다 안드로이드는 중단된다.

백그라운드에서 요청하고, UI 스레드에서 업데이트할 것

두 가지를 합쳐서 서비스 호출을 구현하려면 백그라운드 스레다가 웹 서비스를 호출하고, 응답이 처리된 후에 UI 스레드에서 업데이트 하도록 해야 한다.

스레드 생성

코틀린은 스레드 생성 과정을 단순화해서 쉽고 간단하게 스레드를 생성할 수 있다. 지금은 단일 스레드만으로도 충분하지만, 이후 과정에서는 CPU 바운드와 I/O 바운드 작업을 모두 효율적으로 수행하기 위해 스레드 풀도 생성할 것이다.

CoroutineDispatcher

코틀린에서는 스레드와 스레드 풀을 쉽게 만들 수 있지만 직접 엑세스하거나 제어하지 않는다는 점을 알아야 한다.
CoroutineDispatcher는 기본적으로 가용성, 부하, 설정을 기반으로 스레드 간에 코루틴을분산하는 오케스트레이터다.
오케스트레이션 : 컴퓨터 시스템과 애플리케이션, 서비스의 자동화된 설정, 관리, 조정을 의미
여기에서는 스레드를 하나만 갖는 CoroutineDispatcher를 생성할 것이며, 거기에 추가하는 모든 코루틴은 그 특정 스레드에서 실행된다. 그렇게 하려면 단 하나의 스레드만 갖는 CoroutineDispatcher를 확장한 ThreadPoolDispatcher를 생성한다.
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking fun main(args: Array<String>) = runBlocking { val netDispatcher = newSingleThreadContext(name = "ServiceCall") val task = GlobalScope.launch(netDispatcher) { printCurrentThread() } task.join() }
Kotlin
복사

디스패처에 코루틴 붙이기

디스패처가 만들어졌고 이제 이 디스패처를 사용하는 코루틴을 시작할 수 있다.

async 코루틴 시작

결과 처리를 위한 목적으로 코루틴을 시작했다면 async()를 사용해야 한다. async()는 디퍼드 코루틴 프레임워크에서 제공하는 취소 불가능한 넌 블로킹 퓨처(non-blocking cancellable future)를 의미하여, T는 그 결과의 유형을 나타낸다.
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.async import kotlinx.coroutines.runBlocking fun main(args: Array<String>) = runBlocking { val task = GlobalScope.async { doSomething() } task.join() println("Completed") } fun doSomething() { throw UnsupportedOperationException("Can't do") }
Kotlin
복사
예외를 통해 애플리케이션 실행이 멈추고 예외 스택이 출력되며 또한 애플리케이션의 종료가 되지 아닐거라고 생각할 수 있다. 그러나 실행해보면 로그에 출력되는 예외 스택은 없으며 애플리케이션도 중단되지 않았고 종료 코드는 성공적으로 실행된 것으로 나타난다.
async() 블록 안에서 발생하는 예외는 그 결과에 첨부되는데, 그 결과를 확인해야 예외를 찾을 수 있다. 이를 위해서 isCancelled와 getCancellationException() 메소드를 함께 사용해 안전하게 예외를 가져올 수 있다.
if (task.isCancelled) { val exception = task.getCancellationException() println("Error with message: ${exception.cause}") } else { println("Success") }
Kotlin
복사
예외를 전파하기 위해서 디퍼드에서 await()을 호출할 수 있다.
fun main(args: Array<String>) = runBlocking { val task = GlobalScope.async { doSomething() } // This code will have the exception be propagated task.await() println("Success") }
Kotlin
복사
그러면 애플리케이션이 비정상적으로 중단된다.
await()를 호출해서 중단되는데 이 경우가 예외를 감싸지 않고 전파하는 감싸지 않은 디퍼드(unwrapping deferred)다.
join()으로 대기한 후 검증하고 어떤 오류를 처리하는 것과 await()를 직접 호출하는 방식의 주요 차이는 join()은 예외를 전파하지 않고 처리하는 반면, await()는 단지 호출하는 것만으로 예외가 전파된다는 점이다.

launch 코루틴 시작

결과를 반환하지 않는 코루틴을 시작하려면 launch()를 사용해야 한다. launch()는 연산이 실패한 경우에만 통보 받기를 원하는 fire-and-forget 시나리오를 위해 설계됐으며, 필요할 때 취소할 수 있는 함수도 함께 제공된다.
fun main(args: Array<String>) = runBlocking { val task = GlobalScope.launch { doSomething() } // This code will have the exception be propagated task.join() println("Success") }
Kotlin
복사
예상한 대로 예외가 스택에 출력되지만 실행이 중단되지 않았고, 애플리케이션은 main()의 실행을 완료했다는 것을 알 수 있다.

코루틴을 시작할 때 특정 디스패처 사용하기

지금까지는 기본 디스패처를 사용했지만 다음과 같이 특정 코루틴 디스패처를 사용할 수 있다.
fun main(args: Array<String>) = runBlocking { val dispatcher = newSingleThreadContext(name = "ServiceCall") val task = GlobalScope.launch(dispatcher) { doSomething() } task.join() }
Kotlin
복사

서비스 호출을 위한 코루틴 생성

GlobalScope.launch(dispatcher) { //fetchRssHeadlines()은 웹에서 xml 형식을 받아 정보를 가져온다. val headlines = fetchRssHeadlines() }
Kotlin
복사

UI요소 추가

UI가 블로킹되면 발생하는 일

override fun onResume() { super.onResume() Thread.sleep(5000) }
Kotlin
복사
이렇게 하면 5초 간 UI 스레드가 블로킹된다. 앱 실행시 하얀 화면이 보이게 된다.
UI 관련 액션이 5초이상 걸리면 안드로이드 앱이 죽는다

UI 디스패처 사용

백그라운드에서 스레드를 실행하기 위해 CoroutineDispatcher를 사용했던 것과 같은 방식으로, 메인 스레드에서 작업을 수행토록 CoroutineDispatcher를 사용할 수 있다.

플랫폼별 UI 라이브러리

kotlin-coroutines-android
kotlin-coroutines-javafx
kotlin-coroutines-swing

안드로이드의 UI 코루틴 디스패처 사용

GlobalScope.launch(dispatcher) { val headlines = fetchRssHeadlines() val newsCount = findViewById<TextView>(R.id.newsCount) GlobalScope.launch(Dispatchers.Main) { newsCount.text = "Fount ${headlines.size} News" } }
Kotlin
복사
안드로이드 UI 디스패처는 Dispatchers.Main을 통해서 사용한다.

비동기 호출자로 감싼 동기 함수

override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) GlobalScope.launch(dispatcher) { loadNews() } }
Kotlin
복사
비동기로 실행되는 코드라는 것을명시적으로 나타내는 좋은 사례로 볼 수 있다.
그러나 UI 스레드에서 loadNews()를 호출하는 부분이 많으면, 유사한 launch() 블록이 코드에 많이 분산돼 코드에 대한 가시성이 떨어진다.

미리 정의된 디스패처를 갖는 비동기 함수

launch()를 포함하고 결과인 Job을 반환하는 함수인 asyncLoadNews() 함수를 작성할 수 있다.
private fun asyncLoadNews() = GlobalScope.launch(dispatcher) { val headlines = fetchRssHeadlines() val newsCount = findViewById<TextView>(R.id.newsCount) launch(Dispatchers.Main) { newsCount.text = "Found ${headlines.size} News" } }
Kotlin
복사
코드를 감쌀 필요 없이 어디에서든지 함수를 호출할 수 있다.
함수가 여러 곳에서 호출될 경우 코드를단순화하지만 백그라운드 스레드에서 강제로 실행되기 때문에 함수이 유연성은 줄어든다.

유연한 디스패처를 가지는 비동기 함수

디스패처를 함수의 선택적 파라미터로 설정해서 함수에 어느 정도의 유연성을 줄 수 있다.
private val defDsp = newSingleThreadContext(name = "ServiceCall") private fun asyncLoadNews(dispatcher: CoroutineDispatcher = defDsp) = GlobalScope.launch(dispatcher) { ... }
Kotlin
복사

더 좋은 방식을 선택하기 위한 방법

코루틴으로 감싼 동기 함수 : 가장 큰 장점은 정말로 명시적이라는 점이지만 이러헥 하면 꽤 장황하고 번거로워진다.
특정 디스패처를 갖는 비동기 함수 : 내용이 덜 장황해지는 반면에, 함수를 호출하는 호출자가 어떤 디스패처를 사용해야 할지 결정 할 수 없어서 유연성이 떨어진다.
유연한 디스패처를 갖는 비동기 함수: 함수를 호출하는 호출자가 어디서든 코루틴을 실행할 수 있지만 여전히 함수에 적절한 이름을 부여하는 것은 개발자의 몫이다.

저자의 의견

플랫폼 제약이 있는가? 안드로이드에서는 UI 스레드에서 네트워크 요청을 할 수 없음을 알고 있기 때문에 네트워킹을 할 때 코드가 잘못된 스레드에서 호출을 시도하지 않도록 비동기 함수를 사용하는 것이 유용하다.
함수가 여러 곳에서 호출되는가? 여러 번 호출돼야 한다면 lauch()나 async() 블록으로 동기 함수를 감싸는 것이 좋다.
함수 호출자가 어떤 디스패처를 사용할 지 결정하기를 원하는가? 경우에 따라 호출자가 무엇을 하려고 하는지 상관없이 특정 코드가 특정 디스패처에서 실행되도록 강제하기를 원한다면 특정 디스패처를 갖는 비동기 함수가 필요하다.
이름이 정확하다고 보장할 수 있는가? 동시성 함수의 명칭이 명확하지 않아서 코드가 중단되는 것이 이름이 장황해지는 것보다 더 나쁘다.
동기와 비동기 구현을 동일한 함수에서 모두 제공할 필요는 없을므로 어떠한 비용이 들어라도 이러한 경우는 피해야 한다.
같은 프로젝트에서 이러한 방법들을 과하게 혼용하지 않는다. 일관성을 위해서 코드 베이스에서 하나의 접근 방법을 사용하도록 최선을 다해야 한다.