[Android/Kotlin] 코루틴을 사용한 비동기작업들의 순서를 지정해보자.

Falco·2022년 5월 17일
1

Android

목록 보기
15/55

Problem

비동기로 수행되는 여러 작업들 중 먼저 해야하는 작업이 있을 때 어떻게 하면 좋을까?

Splash화면에서 호흡기 관련 병원정보를 공공 API에서 받아오는 과정에서 문제가 발생하였다.

데이터를 가져오는 과정은 이렇다.

  1. 서버쪽 DB에 공공API 최신 정보가 바뀔때 마다 Version Data를 Update해준다.
  2. 프론트는 Splash화면에서 로컬의 Version Data와 서버의 Version 데이터 만을 비교하여 동일하지 않으면 병원정보를 업데이트한다.
    -> 그렇지 않으면 MainActivity로 전환

문제는 여기서 발생한다.

Splash화면에서 로컬의 Version Data를 Datastore로 저장하였고, 이는 무조건 코루틴이나 Rxjava2와 같은 비동기로 값을 읽어와야한다.

이러한 비동기 작업들의 수행은 로컬 Version Data를 가져오기 전에 서버 Version Data와 비교해서 버전이 동일해도 병원정보를 계속 들고 오는 문제가 발생해 버렸다.

각각의 비동기작업들의 순서를 지정해야할 필요가 있었다.

Solution

TAG : coroutine ordering, 코루틴 순서

코틀린에도 JS와 같은 Async, await가 존재하였지만, 이번에는 runBlocking이라는 것을 사용하여 해결해 볼 것이다.

RunBlocking??
코루틴 빌더인 runBlocking 을 사용하면 내부 코루틴이 완료될 때까지 메인 스레드가 blocking한다.

이말은 즉

만약 UI를 만지는 메인쓰레드에서 runBlocking을 호출하면 UI스케줄러를 Blocking하고 내부코루틴을 동작한다. 이때 이 시간이 길어지면 ANR(Application Not Responding)오류를 발생한다.

joinBlocking 함수내에서
Thread.interrupted()가 일어나기 전까지 + inCompleted되기 전까지 계속 다른 Thread를 Blocking하는 것을 볼 수 있다.

runBlocking은
새 코루틴을 실행하고 완료될 때까지 현재 스레드를 중단 없이 차단합니다. 이 기능은 코루틴에서 사용하면 안 됩니다. 메인 기능과 테스트에서 사용할 수 있도록 서스펜딩 스타일로 작성된 라이브러리에 일반 차단 코드를 브리지하도록 설계되었다.

이와같이 다른 쓰레드를 모두 차단해버리기 때문에 사용하지 않는 편이 좋다고 한다.

정말 꼭 필요한 케이스를 제외한다면 사실 필요치 않다.
Lifecycle을 따르도록 작업하거나, CoroutineScope().launch {}를 통해 충분히 해결할 수 있기 때문이다.

그럼에도
UI업데이트도 없고 타 코루틴이 돌아가지 않는 Splash화면에서는 쓸만하지 않을까?라고 생각하여 코루틴의 job과 join과 같이 사용해 보았다.

Job과 join

launch() 함수로 시작된 코루틴 블록은 Job 객체를 반환합니다.

val job : Job = launch {
    ...
}

반환받은 Job 객체로 코루틴 블록을 취소하거나, 다음 작업의 수행전 코루틴 블록이 완료 되기를 기다릴수 있습니다.

사용해보기

override suspend fun onUpdateMapSuccess(date: String): Unit = runBlocking {
		// join() : Job의 실행이 끝날 때 까지 대기 (launch)
        Log.d("SplashTest", "0")
        var tempdate = ""
        val job = launch {
            Log.d("SplashTest", "1")
            tempdate = CovidRespiratorycareApp.getInstance().getDataStore().text.first()
        }

        job.join()
        Log.d("SplashTest", "2")
        if (date == tempdate) {
            Handler(Looper.getMainLooper()).postDelayed({
                val intent = Intent(this@SplashActivity, MainActivity::class.java)
                intent.flags =
                    Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
                startActivity(intent)
                binding.splashLoadingTv.text = "최신 정보 업데이트 완료"
            }, 1000)
        } else {
                mappingService.getHospitalInfo()
                CovidRespiratorycareApp.getInstance().getDataStore().setText(date)
        }
    }

지금 블로그 글을 쓰면서 내 소스를 다시 보니 이상하다. 왜 굳이 UI Thread 및 다른 쓰레드까지 멈춰가면서 runBlocking을 써야하지?? job, wait, async함수만 사용해도 간단히 해결될 일이 아닌가?

runblocking부분을 그냥 코루틴으로 바꿔주고 실행해 보았다.

override suspend fun onUpdateMapSuccess(date: String) = CoroutineScope(Dispatchers.IO).launch {
//        join() : Job의 실행이 끝날 때 까지 대기 (launch)
        Log.d("SplashTest", "0")
        var tempdate = ""
        val job = launch {
            Log.d("SplashTest", "1")
            tempdate = CovidRespiratorycareApp.getInstance().getDataStore().text.first()
        }

        job.join()
        Log.d("SplashTest", "2")
        if (date == tempdate) {
            Handler(Looper.getMainLooper()).postDelayed({
                val intent = Intent(this@SplashActivity, MainActivity::class.java)
                intent.flags =
                    Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
                startActivity(intent)
                binding.splashLoadingTv.text = "최신 정보 업데이트 완료"
            }, 1000)
        } else {
                mappingService.getHospitalInfo()
                CovidRespiratorycareApp.getInstance().getDataStore().setText(date)
        }
    }

    override fun onUpdateMapFailure(code: Int, message: String) {
    }

잘 작동하는 모습을 볼 수 있다.

그럼 위의 소스와 바뀐 소스의 차이점은 무엇일까?

coroutineScope() 함수 나 withContext() 함수 를 이용해 작성되는 코드 블록은 일시 중단 블록으로 동작하기 때문에 스레드를 차단하지 않는다.

하지만 runBlocking으로 함수를 돌리면 이는 다른 쓰레드를 일시 중단이 아니라 원천 봉쇄하기 때문에 ANR과 같은 오류가 발생할 수 있다.

그렇다 애초에 runBlocking을 사용할만한 상황이 아니였다.

그럼으로 코루틴의 순서를 보장하고 싶을때는 runBlocking보다 job, join을 사용하거나 async, await를 사용한 Deffered 타입을 사용하자.

정리 : runBlocking() 함수는 사용하지 않는것이 좋겠다.

코루틴에서 runblocking을 사용하지 맙시다.
runblocking에 대해
job과 비동기 순서처리에 대해

profile
강단있는 개발자가 되기위하여

0개의 댓글