공부일지

10. Android 코루틴 공부하기 (1)

young313 2022. 8. 28. 16:13

안드로이드 개발자라면 한번은 들어보았을 코루틴 (Coroutine)에 대해 요즘 제대로 공부하고 있다. 현재 개발을 완료한 어플에도 코루틴을 아주 많이 사용하였는데 왜 지금 다시 공부를 하냐고 묻는다면 비정상 종료 로그를 보고 충격을 받았기 때문이다. 구글 플레이스토어에서 제공해주는 비정상정 종료 및 ANR 로그를 분석하던 중 동일한 코드에서 100건이 훌쩍 넘는 오류 로그가 발생한 것을 확인해봤더니 coroutine과 관련한 것이었다. 주먹구구식으로 해결해보려 하였으나 kotlin에서 주요 기능으로 뽑히는 coroutine을 오류를 해결하는 과정에서 조차 대충 분석하면 의미가 없을 것 같아 본격적으로 공부를 시작했다. 그리고 시작하기 전과는 다르게 정말 재밌게 공부하고 있다. 왜 혁신이라고 불리는지, 왜 많은 개발자가 관심을 보이는지 바로 알 만큼 정말 매력적이었다. 

 


Coroutine은 간단하게 비동기 코드를 작성하고 관리할 수 있게 한다. 공식문서에서는 다음과 같이 설명하고 있다. 

 

코루틴은 비동기적으로 실행되는 코드를 간소화하기 위해 Android에서 사용할 수 있는 동시 실행 설계 패턴입니다. 코루틴은 Kotlin 버전 1.3에 추가되었으며 다른 언어에서 확립된 개념을 기반으로 합니다. Android에서 코루틴은 기본 스레드를 차단하여 앱이 응답하지 않게 만들 수도 있는 장기 실행 작업을 관리하는 데 도움이 됩니다. 

 

비동기 실행을 위해서는 주로 callback 함수를 사용한다. 앞의 작업에 뒤의 작업이 영향을 받는다면 callback 함수를 통해 순서를 정해주고 순차적으로 수행될 수 있도록 하는 것이 기본적인 지식일 것이다. 하지만 이런 callback 함수의 반복은 callback 지옥이라 불리는 현상을 초래할 수 있고 kotlin에서는 이를 callback 함수가 아닌 coroutine으로 해결할 수 있다. 

 


공식 문서들의 코드가 내게는 잘 와닿지 않아서 실제 내가 작업하고 있는 코드를 이용해 정리하겠다.

 

fun getBanner(){

    bannerDefault.visibility = View.INVISIBLE
    //1
    total_banner.text = bannerModel.num.toString()
    bannerPager.offscreenPageLimit = bannerModel.num
    bannerPager.adapter = BannerViewPagerAdapter(mContext, bannerModel.uriList)
    bannerPager.orientation = ViewPager2.ORIENTATION_HORIZONTAL
    
 }

 

내가 하고 싶은 작업은 위와 같다. bannerModel 이라는 viewModel에서 값을 불러와서 bannerPager라는 viewPager에 띄우는 코드이다. 이렇게 실행하게 되면 어플이 죽게 된다. 이유는 간단하다. 해당 코드에서 viewModel에 banner image를 저장하는 function은 MainActivity에서 실행되는데, MainActivity 위의 Fragment에 있는 위의 getBanner( ) function이 viewModel에 값이 모두 저장되기 전에 UI를 변경하려하기 때문이다. 즉 코드 속의 bannerModel이 아직 정의되기 전에 코드의 1번 파트가 실행되기 때문이다. 

 

 

이러한 경우 coroutine을 사용하여 "Viewmodel이 정의되고 난 후에 UI를 바꿔라"라는 명령을 보낼 수 있다. 

 

CoroutineScope(Dispatchers.Main).launch {

            Log.d(TAG, "########coroutine not done ${System.currentTimeMillis()}")
            //suspend fun
            getBannerImg(bannerModel)
            Log.d(TAG, "########coroutine done ${System.currentTimeMillis()}")
			
            //1
            bannerDefault.visibility = View.INVISIBLE
            total_banner.text = bannerModel.num.toString()
            bannerPager.offscreenPageLimit = bannerModel.num
            bannerPager.adapter = BannerViewPagerAdapter(mContext, bannerModel.uriList)
            bannerPager.orientation = ViewPager2.ORIENTATION_HORIZONTAL
           

        }


private suspend fun getBannerImg(bannerModel : BannerViewModel){
        withContext(Dispatchers.IO){
            while (bannerModel.uriList.size == 0) { }                        
        }
    }

 

위의 코드에서 suspend fun은 suspend 함수를 말하며 이 함수가 실행되면 코드가 멈추게 되고, 그 함수의 작업이 끝나야만 다시 멈췄던 시점부터 시작된다. 즉 suspend 함수인 getBannerImg는 ViewModel의 사이즈가 0이 아닐때, MainActivity에서 ViewModel로 값을 모두 넣은 것을 확인한 후에 실행이 종료되며 그 이후부터 메인 코드의 1 시점부터 실행이 다시 시작된다. 

 

 

하지만 메인 코드에서 실행이 멈추게 되면 어플이 죽는것이 아닌가?하는 궁금증이 생길 수 있다. 이때 필요한 것이 withContext( )이다. withContext( )가 포함된 함수는 메인 스레드에서 실행되지만 withContext( )를 이용해 IO스레드에서 suspend 함수가 동작하여 그 값을 메인으로 보내줄 수 있도록 한다. 이렇게 되면 메인 스레드는 문제없이 보호될 수 있다. 

 

 

위의 코드를 실행하면 Log가 찍히는 것을 보고 한번 더 코루틴의 성능을 확인할 수 있는데, 다음과 같이 약 1초동안 suspend함수가 실행되고 다시 메인 코드가 실행되는 것을 볼 수 있다. 

 

D/ContentValues: ########coroutine not done 1660915586123
D/ContentValues: ########coroutine done 1660915587436