깨알 개념/Kotlin

[Kotlin] Coroutines - 스레드 전환

interfacer_han 2024. 2. 19. 18:47

본 게시글의 Coroutine 개념은 Android 내에서 사용되는 것을 전제로 작성되었다.

 

#1 Background Thread의 한계 - UI 조작 불가능

...

class MainActivity : AppCompatActivity() {
    ...

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        btnDwCoroutine.setOnClickListener {
            CoroutineScope(Dispatchers.IO).launch {
                downloadData()
            }
        }
    }

    private fun downloadData() {
        for (i in 1..100000) {
            tvProgress.text = "진행도: $i" // 에러 발생
        }
    }
}

/* 에러 메시지
android.view.ViewRootImpl$CalledFromWrongThreadException:
    Only the original thread that created a view hierarchy can touch its views.
    Expected: main
    Calling: DefaultDispatcher-worker-1
*/

위 코드는 "뷰 계층 구조를 만든 원래 스레드만 해당 뷰를 조작할 수 있습니다."라는 에러 메시지를 뱉는 에러를 발생시킨다. UI를 다루는 객체인 Activity는 main 스레드에서 만들어진다. 그러나 downloadData()는 IO 스레드에서 수행된다. main 스레드와 달리, UI에 대한 접근 권한이 없는 IO 스레드에서 TextView를 조작할 수 없는 것이다. 안드로이드에서는 메인 스레드 이외의 스레드를 일반적으로 '백그라운드 스레드'라고 부른다. 정리하면, 백그라운드 스레드에선 UI를 조작할 수 없다는 결론이 나온다.
 
즉 위 코드가 작동되게끔 수정하려면, UI를 조작할 때 잠시 main 스레드로 전환하게 만들어야 한다.
 

#2 스레드 전환 구현

#2-1 수정할 샘플 앱

 

[Kotlin] Coroutines - 기초

본 게시글의 Coroutine 개념은 Android 내에서 사용되는 것을 전제로 작성되었다. #1 다중 스레드 구현 #1-1 코루틴 Android의 Kotlin 코루틴 | Android Developers Android의 Kotlin 코루틴 컬렉션을 사용해 정리하기

kenel.tistory.com

위 게시글의 '완성된 앱'을 수정해서, 스레드를 전환하는 동작을 구현해본다.

 

#2-2 activity_main.xml에 TextView 추가

'Download Data' 및 'Download With Coroutine' 버튼 아래에, android:id="@+id/tvProgress"인 TextView 위젯을 추가한다. 그리고 'Download Data' 버튼은 이제 사용하지 않을 것이므로 삭제한다.

 

#2-3 MainActivity.kt에서 위젯 인플레이트

...

class MainActivity : AppCompatActivity() {
    ...
    private lateinit var tvProgress: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        tvProgress = findViewById(R.id.tvProgress)

        ...
    }

    ...
}

#2-2에서 추가한 텍스트뷰인 'tvProgress'를 inflate한다. 'Download Data' 버튼과 관련된 코드는 삭제한다.

 

#2-4 MainActivity.kt에서 withContext() 사용하기

private suspend fun downloadData() {
    for (i in 1..100000) {
        withContext(Dispatchers.Main) {
            tvProgress.text = "진행도: $i"
        }
    }
}

downloadData() 함수에 #1에서 에러가 났었던 코드 tvProgress.text = "진행도: $i"를 넣고 withContext(Dispatchers.Main) { ... }로 감싼다. withContext()는 코루틴의 실행 스레드를 변경하는 함수다. withContext()는 Suspending Function이고, Suspending Function은 Suspend Function에서만 호출할 수 있으므로, downloadData()에 suspend 키워드를 붙여준다.

 

참고로 withContext { ... } 블록은 별도의 임시 CoroutineScope를 생성한다. 블록 내에서 실행되는 코드는 이 CoroutineScope에서 실행되는데, 이 말은 즉 withContext는 Structured Concurrency가 보장된다는 말이 된다.

#2-5 작동 확인

 

#3 요약

withContext()로 필요할 때만 잠시 스레드를 전환할 수 있다.

 

#4 완성된 앱

https://github.com/Kanmanemone/android-practice/tree/master/coroutines/ThreadSwitch