UI스레드 관련 작업을 하면서 생길 수 있는 에러중에는 CalledFromWrongThreadException과 NetworkOnMainThreadException이 있다.
안드로이드는 뷰 계층을 생성하지 않은 스레드가 관련 뷰를 업데이트하려고 할때 이 Exception을 발생시킨다.
실제로 이 Exception은 UI스레드가 아닌 다른 스레드에서 뷰를 업데이트할때 발생한다.
따라서 이 에러가 발생했다면 UI업데이트는 UI스레드에서 실행시키자.
이 경우는 CalledFromWrongThreadException과 반대로 UI스레드에서 네트워킹 작업을 했을때 발생한다.
네트워킹 작업이 UI스레드에서 일어나면 UI스레드가 블로킹되면서 애니메이션이나 기타 상호작용을 포함한 모든 UI가 멈추게 된다.
따라서 이 에러가 발생했다면 UI스레드에서 진행되던 네트워킹 작업을 백그라운드 스레드를 사용해야한다.
네트워킹 작업은 백그라운드에서 요청하고, 응답이 처리된 후 UI스레드에서 UI업데이트를 진행하면 된다.
코틀린에서는 스레드와 스레드 풀을 쉽게 만들 수 있지만 직접 엑세스하거나 제어하지 않는다는 점을 알아야 한다.
여기서 CoroutineDispatcher를 만들어야하는데, CoroutineDispatcher는 기본적으로 가용성, 부하, 설정을 기반으로 스레드 간에 코루틴을 분산하는 오케스트레이터이다.
디스패처를 만들고나면, 이 디스패처를 사용하는 코루틴을 만들 수 있고, 디스패처는 코루틴이 정의한 스레드를 강제로 사용하도록 할것이다.
코루틴을 시작하는 2가지 방법을 알아볼건데, 결과와 에러를 처리하려면 두가지의 차이점을 알아야한다.
결과 처리를 위한 목적으로 코루틴을 사용했다면 async()를 사용하면 된다.
async()는 Deferred<T>를 반환한다. 따라서 async를 사용할 때 결과를 처리하는것을 잊으면 안된다!
fun main(args: Array<String>) = runBlocking {
val task = GlobalScope.async {
doSomething()
}
task.join()
println("Completed")
}
// 예외를 던진다.
fun doSomething() {
throw UnsupportedOperationException("Can't go")
}
위 코드를 보면 단순하게 생각했을때, 예외가 던져지면서 실행이 멈추고 예외 스택이 출력되며, 애플리케이션 종료코드가 0 이 아닐것이라 생각할 수 있다.
하지만 실행하면 아래와 같이 뜬다.
Completed
Process finished with exit code 0
보면 애플리케이션이 잘 종료되고 종료코드도 0이 나온다. (0은 오류가 발생하지 않았다는 뜻)
async() 안에서 발생하는 예외는 그 결과에 첨부 되는데, 따라서 그 결과를 확인해야 예외를 확인 할 수 있다.
예외를 막는 코드는 아래처럼 할 수 있다.
fun main(args: Array<String>) = runBlocking {
val task = GlobalScope.async {
doSomething()
}
task.join()
if(task.isCancelled) { //취소 됐는지 확인.
val exception = task.getCancellationException()
println("Error with message: ${exception.cause}")
}
println("Completed")
}
// 예외를 던진다.
fun doSomething() {
throw UnsupportedOperationException("Can't go")
}
isCancelled 와 getCancellationException()을 통해 ExceptionHandling을 할 수 있다.
이렇게 처리한 후에는 아래와 같이 뜬다. 예외로 인해 애플리케이션이 멈추진 않는다.
Error with message : Can't do
Process finished with exit code 0
여기서 예외를 전파하고 싶으면 join()이 아닌 await()를 호출할 수 있다.
await()를 호출하게되면 애플리케이션이 비정상적으로 종료하게 된다.
Exception in thread "main". java.lang.UnsuppportedOperationException: Can't go
at chapter2.async.AsyncKt.doSomething(async.kt:30)
...
이렇게 await()를 호출해서 중단되는데 이 경우를 unwrapping deferred라고 한다.
join()으로 대기한 후 검증하고 어떤 오류를 처리하는 것과, await()를 직접 호출하는 방식의 차이는 join()은 에러를 전파하지 않는 반면 await()는 호출하는 것만으로 에러가 전파된다는 점이 있다.
join() 은 async를 통해서 반환된 값을 갖지않고, 그저 끝났는지 여부만 알고있기 때문에(cancel을 위한) 에러가 전파되지 않는다.
await()는 async를 통해서 처리된 값을 갖고있기 때문에, 에러가 전파되는것이다.
따라서 async는 반환하는게 있기때문에 join() await() 둘다 쓸수있지만 launch는 반환하는게 없기때문에 join()만 쓸수 있다!
결과를 반환하지 않는 코루틴은 launch()를 사용하면 된다.
launch()는 연산이 실패한 경우에만 통보를 받는 Fire-and-forget시나리오를 위해 설계되어있으며, 필요할 때 취소할 수도 있다.
Fire-and-Forget 시나리오란 ?
이벤트나 메시지 기반 시스템에서 널리 활용되는 패턴으로, 미사일을 발사(Fire)하고 나면 그 이후 미사일에 대해 잊고(Forget) 있어도 알아서 표적에 명중한다는 것으로, 실행 후 결과에 대해서 신경 쓸 필요가 없는 경우 같은 시나리오를 의미한다.
fun main(args: Array<String>) = runBlocking {
val task = GlobalScope.launch {
doSomething()
}
task.join()
println("Completed")
}
// 예외를 던진다.
fun doSomething() {
throw UnsupportedOperationException("Can't go")
}
위 코드를 실행시, 예상한대로 예외가 출력이 되지만, 실행이 멈추진 않고, main()의 실행을 완료한다.
Exception in thread "ForkJoinPool.commonPool-worker-1" java.lang.UnsuppportedOperationException: Can't go
at chapter2.async.AsyncKt.doSomething(launch.kt:9)
...
completed
위에 사용한 async()와 launch()는 모두 기본 디스패처를 사용하고 있었다. 현재 사용중인 쓰레드의 이름을 확인하는 방법은 아래와 같다.
fun main(args: Array<String>) = runBlocking {
val task = launch {
printCurrentThread()
}
task.join()
}
fun printCurrentThread() {
println("Running in thread ${Thread.currentThread().name}")
}
위 코드를 실행하면 기본적으로 코루틴이 DefaultDispatcher에서 실행됨을 알 수 있다. (지금은 다를 수 있음)
커스텀한 Dispatcher를 만들고 한번 확인해보자.
fun main(args: Array<String>) = runBlocking {
val dispatcher = newSingleThreadContext(name = "ServiceCall")
val task = launch(dispatcher) {
printCurrentThread()
}
task.join()
}
fun printCurrentThread() {
println("Running in thread ${Thread.currentThread().name}")
}
위 코드의 출력은 아래와 같다.
Running in thread ServiceCall
Process finished with exit code 0
코루틴을 이용하여 자바의 DocumentBuilder를 사용해 RSS 피드의 갯수를 호출해보자.
기본 MainActivity에 아래 코드를 작성한다.
스레드를 하나만 갖는 Dispatcher를 생성할것이며, 거기에 추가하는 모든 코루틴은 그 Dispatcher에서 실행될 것이다.
private val dispatcher = newSingleThreadContext(name = "ServiceCall")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
GlobalScope.launch(dispatcher) {
//TODO
}
}
자바의 DocumentBuilder를 사용할 것이기 때문에 DocumentBuilderFactory를 담을 변수를 추가하고, 실제 호출을 수행할 함수를 만든다.
private val dispatcher = newSingleThreadContext(name = "ServiceCall")
private val factory = DocumentBuilderFactory.newInstance()
...
private fun fetchRssHeadlines(): List<String> {
val builder = factory.newDocumentBuilder()
val xml = builder.parse("https://www.npr.org/rss/rss.php?id=1001")
return emptyList()
}
이후 onCreate() 안에 GlobalScope.launch(dispatcher)안에 fetchRssHeadlines()를 넣는다.
GlobalScope.launch(dispatcher) {
val headlines = fetchRssHeadlines()
}
이후 실제로 응답의 Body를 읽고 헤드라인을 반환하는 코드를 작성한다.
여기서는 Coroutine이 중요하므로 아래 예제를 따라하도록... 코드는 단순히 XML의 모든 요소를 검사하면서 각 피드에 있는 article의 title을 제외한 모든것을 필터링한다.
private fun fetchRssHeadlines(): List<String> {
val builder = factory.newDocumentBuilder()
val xml = builder.parse("https://www.npr.org/rss/rss.php?id=1001")
val news = xml.getElementsByTagName("channel").item(0)
return (0 until news.childNodes.length)
.map { new.childNodes.item(it) }
.filter { Node.ELEMENT_NODE == it.nodeType }
.map { it as Element }
.filter { "item" == it.tagName }
.map {
it.getElementsByTagName("title").item(0).textContent
}
}
이후 R.layout.activity_main에 뉴스의 수량을 표시해주기 위한 TextView를 추가해준다. (코드 생략, id 는 newsCount)
GlobalScope.launch(dispatcher) {
val headlines = fetchRssHeadlines()
val newsCount = findViewByid<TextView>(R.id.newsCount)
newsCount.text = "Found ${headlines.size} News"
}
하지만 위처럼 작성하면 발생하는 에러가 있다.
그건 바로 이 글의 처음에 있던 CalledFromWrongThreadException이다. 코루틴의 내용이 현재 백그라운드에서 실행되고 있고, UI업데이트인 newsCount.text = "Found ${headlines.size} News" 는 UI 스레드에서 일어나야 하기 때문이다.
백그라운드 스레드 코드 내에서 UI스레드에서 업데이트 시키는 방법은 아주 간단하다.
UI Coroutine Dispatcher인 Dispatcher.Main을 사용하면 된다.
GlobalScope.launch(dispatcher) {
val headlines = fetchRssHeadlines()
val newsCount = findViewByid<TextView>(R.id.newsCount)
GlobalScope.launch(Dispatchers.Main) {
newsCount.text = "Fount ${headlines.size} News"
}
}
현재 뉴스의 수량을 요청하고 표시하는 코드가 onCreate() 함수 안에 있다. 이런경우 activity 생성부분과 혼재돼 있을 뿐 아니라, 코드 재사용이 어렵다. 또한 새로고침이 생긴경우 코드를 또 써야한다.
그래서 지금부터 가장 일반적으로 코루틴을 별도 함수로 분리하는 방법을 소개한다.
첫번째 방법은 아주 심플하다. 제목만 봐도 이해가 된다. 일단 GlobalScope.launch(dispatcher) 내부 코드를 함수로 분리한다.
private fun loadNews(){
val headlines = fetchRssHeadlines()
val newsCount = findViewByid<TextView>(R.id.newsCount)
GlobalScope.launch(Dispatchers.Main) {
newsCount.text = "Fount ${headlines.size} News"
}
}
이렇 게 동기방식으로 만들어진 코드를 onCreate내부에서 GlobalScope.launch(dispatcher)를 만들어서 호출해준다.
감싸 주지않으면 NetworkOnMainThreadException이 발생한다.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
GlobalScope.launch(dispatcher) {
loadNews()
}
}
이렇게 해주면 비동기로 실행되는 코드라는걸 명시적으로 알려줄 수 있다. 또한 loadNews()를 호출하는 호출자가 이미 백그라운드 스레드에 있다면 launch()를 사용하지 않아도 같은 백그라운드에서 불러올 수 있어서 꽤나 유연하다.
하지만 UI스레드에서 loadNews()를 호출하는 부분이 많다면 ㅂ..비..비추 한다 ( launch()를 계속 써야해서 )
두번째 방법으로는 launch()를 포함하고 이미 정의된 dispatcher를 사용하고 결과인 Job을 반환하는 방식이다.
이방법의 장점은 스레드와 상관없이 호출할 수 있고, 반환받은 Job을 통해서 호출자가 취소 시킬 수 있다.
private fun asyncLoadNews() = GlobalScope.launch(dispatcher) {
val headlines = fetchRssHeadlines()
val newsCount = findViewByid<TextView>(R.id.newsCount)
launch(Dispatchers.Main) {
newsCount.text = "Fount ${headlines.size} News"
}
}
이 방법의 장점은 코드를 감쌀 필요없이 어디서든 호출할 수 있다는 장점이 있지만. 백그라운드 스레드에서 강제로 실행되기 때문에 함수의 유연성은 줄어든다.
또한 코드 가독성에 있어서 네이밍을 async하다는걸 잘 인지할 수 있게 네이밍을 해야한다. (async 붙이면 될듯)
하지만 여기서 유연한 디스패처를 갖는 비동기 함수를 만들 수 있다.
private val defDsp = newSingleThreadContext(name= "ServiceCall")
private fun asyncLoadNews(dispatcher: CoroutineDispatcher = defDsp) = GlobalScope.launch(dispatcher) {
...
}
하지만 단점은 네이밍이 중요하다는 점...
위에 3가지 옵션들이 있지만, 최선의 결정은 상황에 따라 달라질 수 있고, 모든 시나리오에 딱 맞는 해결 방법은 없다. 잘 생각해보고 쓰자.
Kotlin Coroutine - (3) Coroutine lifeCycle And Error Handling (0) | 2022.03.28 |
---|---|
Kotlin Coroutine - (1-3) 코틀린에서의 동시성 (0) | 2022.02.15 |
Kotlin Coroutine - (1-2) CPU 바운드와 I/O 바운드, 동시성 프로그래밍의 어려운점. (0) | 2022.02.13 |
안드로이드 Drag & Drop 해보기. (0) | 2022.01.19 |
Kotlin Coroutine - (1-1) 기초와 동시성에 대해 (0) | 2022.01.09 |
댓글 영역