ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Study] 코틀린 동시성 프로그래밍 - 2장. 코루틴 인 액션
    LANGUAGE/KOTLIN 2022. 8. 12. 17:43

    안드로이드의 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()
    }
    

    디스패처에 코루틴 붙이기

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

    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")
    }
    

    예외를 통해 애플리케이션 실행이 멈추고 예외 스택이 출력되며 또한 애플리케이션의 종료가 되지 아닐거라고 생각할 수 있다. 그러나 실행해보면 로그에 출력되는 예외 스택은 없으며 애플리케이션도 중단되지 않았고 종료 코드는 성공적으로 실행된 것으로 나타난다.

    async() 블록 안에서 발생하는 예외는 그 결과에 첨부되는데, 그 결과를 확인해야 예외를 찾을 수 있다. 이를 위해서 isCancelled와 getCancellationException() 메소드를 함께 사용해 안전하게 예외를 가져올 수 있다.

    if (task.isCancelled) {
        val exception = task.getCancellationException()
        println("Error with message: ${exception.cause}")
    } else {
        println("Success")
    }
    

    예외를 전파하기 위해서 디퍼드에서 await()을 호출할 수 있다.

    fun main(args: Array<String>) = runBlocking {
        val task = GlobalScope.async {
            doSomething()
        }
    
        // This code will have the exception be propagated
        task.await()
    		println("Success")
    }
    

    그러면 애플리케이션이 비정상적으로 중단된다.

    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")
    }
    

    예상한 대로 예외가 스택에 출력되지만 실행이 중단되지 않았고, 애플리케이션은 main()의 실행을 완료했다는 것을 알 수 있다.

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

    지금까지는 기본 디스패처를 사용했지만 다음과 같이 특정 코루틴 디스패처를 사용할 수 있다.

    fun main(args: Array<String>) = runBlocking {
    		val dispatcher = newSingleThreadContext(name = "ServiceCall")
        val task = GlobalScope.launch(dispatcher) {
            doSomething()
        }
    
        task.join()
    }
    

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

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

    UI요소 추가

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

    override fun onResume() {
    	super.onResume()
    	Thread.sleep(5000)
    }
    

    이렇게 하면 5초 간 UI 스레드가 블로킹된다. 앱 실행시 하얀 화면이 보이게 된다.

    <aside> 💡 UI 관련 액션이 5초이상 걸리면 안드로이드 앱이 죽는다

    </aside>

    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"
      }
    }
    

    <aside> 💡 안드로이드 UI 디스패처는 Dispatchers.Main을 통해서 사용한다.

    </aside>

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

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    		GlobalScope.launch(dispatcher) {
    			loadNews()
    		}
    }
    

    비동기로 실행되는 코드라는 것을명시적으로 나타내는 좋은 사례로 볼 수 있다.

    그러나 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"
        }
    }
    

    코드를 감쌀 필요 없이 어디에서든지 함수를 호출할 수 있다.

    함수가 여러 곳에서 호출될 경우 코드를단순화하지만 백그라운드 스레드에서 강제로 실행되기 때문에 함수이 유연성은 줄어든다.

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

    디스패처를 함수의 선택적 파라미터로 설정해서 함수에 어느 정도의 유연성을 줄 수 있다.

    private val defDsp = newSingleThreadContext(name = "ServiceCall")
    private fun asyncLoadNews(dispatcher: CoroutineDispatcher = defDsp) = 
    	GlobalScope.launch(dispatcher) { ... }
    

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

    코루틴으로 감싼 동기 함수 : 가장 큰 장점은 정말로 명시적이라는 점이지만 이러헥 하면 꽤 장황하고 번거로워진다.

    특정 디스패처를 갖는 비동기 함수 : 내용이 덜 장황해지는 반면에, 함수를 호출하는 호출자가 어떤 디스패처를 사용해야 할지 결정 할 수 없어서 유연성이 떨어진다.

    유연한 디스패처를 갖는 비동기 함수: 함수를 호출하는 호출자가 어디서든 코루틴을 실행할 수 있지만 여전히 함수에 적절한 이름을 부여하는 것은 개발자의 몫이다.

    저자의 의견

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

    댓글

Designed by Tistory.