최데브는 오늘도 프로그래밍을 한다.

안드로이드 코루틴 - async와 await, LifecycleScope과 ViewModelScope 본문

Android

안드로이드 코루틴 - async와 await, LifecycleScope과 ViewModelScope

최데브 2022. 1. 11. 22:36
반응형

코루틴을 사용하다보면 비동기적으로 동작하는 예를 들면 네트워크 작업 같은게 있다고 예를 들어보자.

 

간단한 코드부터 보자.

 

class MainActivity : AppCompatActivity() {
 
    val TAG = "MainActivity"
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
 
        GlobalScope.launch(Dispatchers.IO) {
            val time = measureTimeMillis {
                val answer1 = networkCall() 
                val answer2 = networkCall2()
                Log.d(TAG, "Answer1 is $answer1")
                Log.d(TAG, "Answer2 is $answer2")
            }
            Log.d(TAG,"Requests took $time ms.") //0 6초
        }
 
        suspend fun networkCall(): String{
            delay(3000L)//3초 딜레이
            return "Answer 1"
        }
 
        suspend fun networkCall2():String{
            delay(3000L)// 3초딜레이
            return "Answer 2"
        }
    }
}

위 코드를 보면 총 합쳐서 6초가 걸린다. 왜냐? 

suspend 펑션이 둘다 3000L 만큼 딜레이를 주기 때문. 즉 작업이 병렬적으로 이루어지지 않고

하나씩 순차적으로 진행한다는 말이다. suspend 가 기본적으로 그렇다. 동작하고 있으면 그걸 진행하는 동안 코루틴이 일시정지되고 함수를 실행을 완료한 다음 그 뒤가 실행된다.

 

그러나 우리는 순서가 중요한 상황이 아닌 동시에 처리해야하는 일을 만날때가 더 많다.(개인적인 생각)

당연히 이런 방법이 코틀린에도 있는데 다음의 예를 보자.

 

GlobalScope.launch(Dispatchers.IO) {
            val time = measureTimeMillis {
                var answer1:String? = null
                var answer2:String? = null
                val job1 = launch { answer1 = networkCall() }//3초후 networkCall()의 return값인 Answer1을 string으로 리턴합니다.
                val job2 = launch { answer2 = networkCall2() }
 
                job1.join()
                job2.join()
                //job1과 job2가 동시에 실행됩니다.
                Log.d(TAG, "Answer1 is $answer1")
                Log.d(TAG, "Answer2 is $answer2")
            }
            Log.d(TAG,"Requests took $time ms.")
            //job1과 job2가 동시에 실행됬기때문에 3만에 모든 작업이 끝난다.
        }
        suspend fun networkCall(): String{
            delay(3000L)
            return "Answer 1"
        }
 
        suspend fun networkCall2():String{
            delay(3000L)
            return "Answer 2"
        }

처음 예시로든 코드와 크게 달라지지 않았다. 달라진접은 job 들에 join을 사용해줬다는 점.

주석에 있듯 병렬적으로 실행하게 된다. join은 job.join이 끝날때까지 현재 코루틴에게 기다리라고 하는 명령어다. suspend에서만 동작한다. 

 

 

하지만 join 보다 더 간편하게 쓸 수 있는게 있다. Async다.

 

위를 보면 Async를 쓰지않고 launch를 사용했는데 launch는 job을 반환하고

Async는 deferred를 리턴한다는 차이점이있다.

 

GlobalScope.launch(Dispatchers.IO) {
 
            val time = measureTimeMillis {
                val answer1 = async { networkCall() }
                val answer2 = async { networkCall2() }
                Log.d(TAG, "Answer1 is ${answer1.await()}")
                Log.d(TAG, "Answer2 is ${answer2.await()}")
            }
            Log.d(TAG, "Request took $time ms.")
        }
        suspend fun networkCall(): String {
            delay(3000L)
            return "Answer 1"
        }
 
        suspend fun networkCall2(): String {
            delay(3000L)
            return "Answer 2"
        }

async  , await 를 사용하니 스레드를 방해하지 않으면서도 값이 나올때까지 기다린다. 이 또한 병렬적으로 실행되어 3초만에 동작이 완료된다.

 

LifecycleScope 는 뭘까? 코루틴을 공부하다보면 스코프라는 개념이 나온다. 

이는 코루틴의 범위? 같은 느낌으로 생각하면 되는데 대표적으로 

  • GlobalScope
  • lifecycleScope
  • ViewModelScope

가 있다.

 

GlobalScope 는 앱 라이프사이클중에 계속해서 살아서 동작하는 스코프다.

 

class MainActivity : AppCompatActivity() {
 
    val TAG = "MainActivity"
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
 
        btnStartActivity.setOnClickListener{
            GlobalScope.launch {
                while (true){ //button눌린게 true면 still running로그 반복 프린트
                    delay(1000L) //1초 간격으로 printing
                    Log.d(TAG,"Still running")
 
                }
                //when new intent was created,
            }
            GlobalScope.launch {
                delay(5000L) //5초후 새로운 intent시작
                Intent(this@MainActivity, SecondActivity::class.java).also{
                    startActivity(it)
                    finish()
                }
            }
        }
    }
 
}

이런 코드가 있다고치면 5초후에 intent가 되어서 다른 화면으로 넘어가도 Still running 이라는 문구가 1초마다 계속 찍히는걸 볼 수 있을 것이다.  앱 실행내내 필요한 동작이 아니라면 쓸모없는 자원을 소모하는 상황이된다.

 

그럼 이 화면을 벗어났을때는 멈추게 하고 싶다면 job에다가 cancle을 써도 될테지만 그보다는 아래와 같이

 

class MainActivity : AppCompatActivity() {
 
    val TAG = "MainActivity"
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
 
        btnStartActivity.setOnClickListener{
            //변경 시작
            lifecycleScope.launch {
            //변경 끝
                while (true){ //button눌린게 true면 still running로그 반복 프린트
                    delay(1000L) //1초 간격으로 printing
                    Log.d(TAG,"Still running")
 
                }
                //when new intent was created,
            }
            GlobalScope.launch {
                delay(5000L) //5초후 intent시작
                Intent(this@MainActivity, SecondActivity::class.java).also{
                    startActivity(it)
                    finish()
                }
            }
        }
    }
 
}

lifecycleScope 을 써주면 intent가 되어서 다른 화면으로 넘어갔을때 자동으로 멈추게 되는걸 알 수 있다.

 

ViewModelScope 를 쓰는 방식은 다른것들과 같다. lifecycleScope 이 있는 자리에 ViewModelScope 로 교체해주면 끝이다.

Viewmodel 에서 사용하며 Viewmodel이 살아있는 동안에만 진행하는 코드다. 네트워크와 관련된 함수를 불러와서 쓰면 될듯하다. MVVM 패턴에서 사용하면 유용할듯하다. 예를 들면 액티비티 위에서 fragment 들이 올라가있고 fragment 사이를 왔다갔다하면서도 모든 fragment 에 해당 값이 공유되어야할때 Viewmodel을 통해서 공유하되 ViewModelScope 를 사용해서 코루틴을 동작시키면 잘 동작 될 것이다.

 

 

반응형
Comments