본문 바로가기
Kotlin

코루틴 Scope는 어떤 종류들이 있을까? (CoroutineScope, LifecycleScope, ViewModelScope)

by 안솝우화 2022. 4. 25.
반응형

오늘은 코루틴을 실행하는 스코프에 대해서 알아보도록 하겠습니다, 우선 지원하는 스코프는 많지만 오늘은 3가지 정도 알아보겠습니다.

설명은 각각의 스코프를 안드로이드 스튜디오에서 선언부로 들어가 주석 설명을 하나씩 해석하며 진행하겠습니다!

 

CoroutineScope

우선 첫번째로 가장 기본적인 CoroutineScope입니다. 스코프를 안드로이드 스튜디오에서 타고 들어가 선언부를 보면 이렇게 되어있습니다

선언부

주석 번역

지정된 코루틴 콘텍스트를 감싸는 코루틴 범위를 만듭니다. 지정된 콘텍스트에 Job 요소가 없으면 기본 Job()이 생성됩니다. 이렇게 하면 이 범위에서 하위 코루틴이 실패하거나 범위 자체를 취소하면 coroutineScope 블록 내와 마찬가지로 모든 스코프의 하위 코루트가 취소됩니다.

 

지정된 코루틴 컨텍스트를 감싸는 코루틴 범위를 만듭니다
          CoroutineScope(Dispatchers.IO).launch {
              //code
          }

말 그대로 CoroutineScope를 생성한다는 의미입니다

 

지정된 컨텍스트에 Job 요소가 없으면 기본 Job()이 생성됩니다

안드로이드 스튜디오에서 변수의 자료형을 확인

Job을 설정하지 않을 시 디폴트로 생성이 된다는 의미입니다

위의 스코프를 변수에 넣고 타입을 확인해보면 자동으로 Job이 생성되는 걸 확인할 수 있습니다

그럼 Job을 설정하는건 어떻게 할까요?

              val job = Job()
              CoroutineScope(Dispatchers.IO + job).launch {
                  //code1
              }
              CoroutineScope(Dispatchers.IO + job).launch {
                  //code2
              }

위의 코드와 같이 설정이 가능합니다, 저렇게 변수로 job을 설정하면 다중 Job 설정이 가능해집니다

예를 들어 job.cancel을 했을 때 두 스코프 모두 영향을 받게 됩니다!

 

 

이렇게 하면 이 범위에서 하위 코루틴이 실패하거나 범위 자체를 취소하면 coroutineScope 블록 내와 마찬가지로 모든 스코프의 하위 코루트가 취소됩니다

스코프안에 다른 스코프가 존재할 때 부모를 캔슬 등을 하면 자연스럽게 안에 자식도 캔슬된다는 것을 의미합니다, 쉽게 생각하면 부모에 영향이 미치면 당연히 자식도 영향을 받게 되는 것과 같다고 보시면 될 것 같습니다

         val job = CoroutineScope(Dispatchers.Main).launch {
             launch(Dispatchers.IO) {
                 for (n in 0..100000){
                     Log.d("TAG","Child scope1 $n")
                 }
             }
             launch(Dispatchers.IO) {
                 for (n in 0..100000){
                     Log.d("TAG","Child scope2 $n")
                 }
             }
         }
         job.cancel()

위의 코드는 원래 모두 로그가 찍히지만 cancel로 아무 로그도 표시되지 않습니다

 

 

LifecycleScope

선언부를 보면 이렇게 되어있습니다

선언부

주석 번역

coroutineScope는 이 라이프사이클 소유자의 라이프사이클과 연계되어 있습니다.
라이프사이클이 destoryed 되면 이 스코프가 취소됩니다.
이 스코프는 Dispatchers.Main.immediate에 바인딩되어있습니다

 

coroutineScope는 이 라이프사이클 소유자의 라이프사이클과 연계되어 있습니다.

lifecycleScope의 스코프는 지정한 라이프사이클과 연계가 된다는 의미입니다

		//activity
		lifecycleScope.launch(Dispatchers.IO) {

        }

이렇게 스코프를 Activity에서 생성하면 자동으로 Activity의 라이프사이클과 연계가 됩니다

 

 

라이프사이클이 destoryed되면 이 스코프가 취소됩니다.

사실 위에 말과 크게 다르지 않습니다. 라이프사이클이 연계되어 destoryed 될 때 자동으로 스코프가 취소됩니다

        lifecycleScope.launch(Dispatchers.IO) {
            for (n in 0..99999){
                Log.d("TAG","$n")
            }
        }

위의 코드에서 출력 중 앱을 종료하면 어떻게 될까요? 정답은 로그가 계속해서 끝까지 찍힙니다

왜냐하면 반복문은 이미 떠나간 배라고 비유하면 이해하기 쉬울 것 같습니다. 이미 시작되어서 취소해도 의미가 없습니다.

그럼 flow의 데이터를 하나씩 찍어보는 코드는 어떨까요??

        val userList = listOf("User1", "User2", "User3", "User4", "User5")
        lifecycleScope.launch(Dispatchers.IO) {
            flow {
                userList.forEach { user ->
                    emit(user)
                    delay(1500)
                }
            }.collect{ user ->
                Log.d("TAG", user)
            }
        }

위의 코드는 flow로 데이터를 1.5초에 하나씩 발행해 줍니다, 그리고 여기서 collect로 받아서 출력해 줍니다

이 코드에서는 도중에 앱을 종료하면 정상적으로 멈추게 됩니다!

 

 

이 스코프는 Dispatchers.Main.immediate에 바인딩되어있습니다

말 그대로 기본적으로 lifecycleScope의 디스패처는 Main.immediate이라는 의미입니다

여기서 Dispatchers.Main.mmediate와 Dispatchers.Main는 서로 다릅니다, 그럼 어떻게 다를까요??

        lifecycleScope.launch {
            Log.d("TAG", "parent : ${Thread.currentThread().name}")
            launch(Dispatchers.IO){
                Log.d("TAG", "child : ${Thread.currentThread().name}")
            }
        }

바로 순서를 보장해줍니다, 위의 코드는 start → parent : ? → end 순서로 로그가 찍힙니다, 그리고 따로 디스패처를 설정해 주지 않았기 때문에 기본으로 Main.immediate에 바인딩 되어있는 상태입니다

        lifecycleScope.launch(Dispatchers.Main) {
            Log.d("TAG", "parent : ${Thread.currentThread().name}")
            launch(Dispatchers.IO){
                Log.d("TAG", "child : ${Thread.currentThread().name}")
            }
        }

그리고 위에 코드는 디스패처를 Main으로 설정해 줬습니다, 일반적인 Main은 순서를 보장해주지 않기 때문에 위의 코드 로그를 확인해 보면 start -> end -> parent : ? 순서로 실행이 되는 걸 확인할 수 있습니다

 

 

ViewModelScope

스코프를 타고 들어가 선언부를 보면 이렇게 되어있습니다

선언부

주석 번역

ViewModel에 연결된 CoroutineScope입니다. ViewModel이 지워지면(즉, ViewModel.onCleared가 호출됨) 이 스코프가 취소됩니다.
이 스코프는 Dispatcher.Main.immediate에 바인딩됩니다.

 

ViewModel에 연결된 CoroutineScope입니다.
class ViewModel : ViewModel() {

    fun viewModelScope() = viewModelScope.launch {

    }
}

말 그대로 ViewModel에 연결이 되어있다는 의미이고 때문에 ViewModel에서만 사용할 수 있습니다

 

 

ViewModel이 지워지면(즉, ViewModel.onCleared가 호출됨) 이 스코프가 취소됩니다.

ViewModel의 onCleared가 호출되면 같이 취소된다는 걸 의미합니다, 즉 ViewModel 라이프사이클과 연관되어 있다고 볼 수 있습니다

    val userList = listOf("User1", "User2", "User3", "User4", "User5")

  	fun viewModelScope() = viewModelScope.launch {
        flow {
            userList.forEach { user ->
                emit(user)
                delay(1500)
            }
        }.collect{ user ->
            Log.d("TAG", user)
        }
    }
    
    //in Activity
    viewModel.viewModelScope()

위 코드는 lifecycleScope에서 사용한 flow를 이용한 데이터 발행 코드와 같습니다

ViewModel은 결국 Activity 라이프사이클을 따라가기 때문에 앱을 실행 후 호출되고 있을 때 앱을 종료하면 (뒤로 가기 버튼 등) 로그가 찍히는 게 멈추는 걸 확인할 수 있습니다

 

 

이 스코프는 Dispatcher.Main.immediate에 바인딩됩니다.

lifecycleScope와 같이 기본적으로 Dispatcher.Main.immediate에 바인딩되어있다는 의미입니다, 즉 lifecycleScope와 같이 순서를 보장해준다는 의미입니다

반응형