본문 바로가기

Coroutine

📌Coroutine(코루틴) 실습 : viewModelScope, launch, suspend, join

728x90

코루틴에 대한 내용을 공부하고 Android에서 사용해오다 깔끔하게 정리해보려 합니다.

 

1. 모듈 수준의 build.gradle 파일에 ViewModel KTX 라이브러리 종속성 추가

def lifecycle_version = "2.4.1"
// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"

viewModelScope() 함수를 제공받기 위해서 ViewModel KTX 라이브러리를 추가합니다.

 

 

2. ViewModel 인스턴스 초기화

class MainActivity : AppCompatActivity() {
    private lateinit var mainViewModel: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
        mainViewModel.exam()
    }
}

 

MainViewModel의 인스턴스를 생성하기 위해서 ViewModelProvider를 사용합니다.

 

기본적인 설정은 끝났습니다. Coroutine의 구조를 간단하게 알아보고 안드로이드 어플리케이션을 제작할 때 Coroutine을 활용하는 방법에 대해서 알아보겠습니다.

 

1. Coroutine Scope

코루틴의 동작하는 범위인 scope를 정의하고 scope 내에서 실행되는 코루틴의 실행을 감시, 취소할 수 있으며, CoroutineContext를 가지고 있습니다.

 

2. Coroutine Context

코루틴을 처리하기 위한 정보로, 코루틴은 항상 CoroutineContext로 구성된 Context 안에서 실행되며 Dispatchers와 Job으로 구성됩니다.

 

3. Coroutine Builder

launch, async 와 같은 코루틴 빌더와 coroutineScope, withContext 등과 같은 스코프 빌더는 CoroutineScope의 확장 함수로 정의되며, 코루틴을 생성할 때 소속된 CoroutineScope에 정의된 CoroutineContext를 기반으로 코루틴을 생성합니다.

 

 

📌 viewModelScope : Coroutine Scope의 일종으로 ViewModel에서 coroutine context를 명시적으로 취소하지 않아도 onCleared() 생명주기 메서드가 호출될 때 coroutine 작업을 취소하며, Dispatchers.Main에 바인딩됩니다.

 

📌 launch : thread를 blocking 하지 않는 코루틴 작업을 실행하고 결과를 반환할 필요가 없는 작업에 사용되며 Job 객체를 반환합니다.

 

① 실습

기본적으로 메서드 내에서 순차적 실행됩니다.

class MainViewModel : ViewModel() {

    fun exam() {
        viewModelScope.launch {
            Log.d("VM","launch: ${Thread.currentThread().name}")
            Log.d("VM", "World!")
        }

        Log.d("VM", "exam: ${Thread.currentThread().name}")
        Log.d("VM", "Hello")
    }
}
실행 결과
D/VM: launch: main
D/VM: World!
D/VM: exam: main
D/VM: Hello

 

② 실습

코루틴이 delay(1) == 0.001초 동안 대기하도록 설정하여 Hello가 먼저 출력됩니다.

 

여기서 대기의 의미는 코루틴이 0.001초 동안 다른 작업이 메인 스레드를 점유해 작업 하는 것을 기다리는 것이지 block하는 것이 아닙니다.

exam()과 코루틴 모두 메인 스레드를 사용하고 있는 것을 확인할 수 있습니다. 코루틴이 0.001초간 메인 스레드를 양보하겠다고 했지만 exam()은 break point가 없기 때문에 코루틴은 exam()의 작업이 끝날 때 까지 대기합니다. 

class MainViewModel : ViewModel() {

    fun exam() {
        viewModelScope.launch {
            delay(1)
            Log.d("VM","launch: ${Thread.currentThread().name}")
            Log.d("VM", "World!")
        }

        Log.d("VM", "exam: ${Thread.currentThread().name}")
        Log.d("VM", "Hello")
    }
}
실행 결과
exam: main
Hello
launch: main
World!

 

 

📌 Dispatchers.IO : 파일 또는 소켓 IO 등의 가볍고 빈번한 작업에 사용되며, 앞서 말했듯 IO 작업은 CPU를 덜 소모하기 때문에 코어 수 보다 훨씬 많은 스레드를 가지는 스레드 풀입니다.

 

③ 실습

코루틴이 실행될 스레드를 worker 스레드를 사용하도록 하는 경우 순차적으로 실행되어 World!가 먼저 출력되던 실습 1과 다르게 Hello가 먼저 출력됩니다.

 

이유는 worker 스레드를 생성하고 이 스레드에서 작업하도록 설정하는 시간이 소요되기 때문에 Hello가 먼저 출력되는 것으로 생각됩니다.

exam()과 코루틴이 서로 다른 스레드를 사용하고 있기 때문에 exam()의 작업과 관련없이 코루틴의 작업이 끝난 시점에 World!가 출력됩니다.

class MainViewModel : ViewModel() {

    fun exam() {
        viewModelScope.launch(Dispatchers.IO) {
            Log.d("VM","launch: ${Thread.currentThread().name}")
            Log.d("VM", "World!")
        }

        repeat(100) {
            Log.d("VM", "exam: ${Thread.currentThread().name}")
            Log.d("VM", "Hello")
        }
    }
}
실행 결과
exam: main
Hello

    ...

launch: DefaultDispatcher-worker-1
World!

    ...

exam: main
Hello

 

④ 실습

코루틴 내부에 새로운 코루틴을 생성했습니다. 결과를 보면 알 수 있듯이 루틴이 메인 스레드를 점유하는 동안 서브루틴이 대기합니다.

class MainViewModel : ViewModel() {

    fun exam() {
        viewModelScope.launch {
            launch {
                Log.d("VM","launch2: ${Thread.currentThread().name}")
                Log.d("VM", "World!")
            }

            Log.d("VM", "launch1: ${Thread.currentThread().name}")
            Log.d("VM", "Hello")
        }
    }
}
실행 결과
launch1: main
Hello
launch2: main
World!

 

⑤ 실습

exam() 메서드에 코루틴이 있고, 코루틴에 2개의 코루틴이 있고 모두 메인 스레드에서 실행됩니다.

실습 1에서와 같이 순차적으로 실행되기 때문에 코루틴이 먼저 실행되고 실습 4에서와 같이 루틴이 메인 스레드를 점유하는 동안 서브루틴이 대기했다가 순차적으로 실행됩니다.

class MainViewModel : ViewModel() {

    fun exam() {
        viewModelScope.launch {
            launch {
                Log.d("VM","launch2: ${Thread.currentThread().name}")
                Log.d("VM", "2")
            }

            launch {
                Log.d("VM","launch3: ${Thread.currentThread().name}")
                Log.d("VM", "3")
            }
            
            Log.d("VM", "launch1: ${Thread.currentThread().name}")
            Log.d("VM", "1")
        }

        Log.d("VM", "exam: ${Thread.currentThread().name}")
        Log.d("VM", "0")
    }
}
실행 결과
launch1: main
1
launch2: main
2
launch3: main
3
exam: main
0

 

 

📌 suspend : 중단 가능한 함수를 의미하며, 함수에서 delay, launch를 포함한 코드를 작성하고 싶은 경우 suspend 키워드 사용 중요한 점은 CoroutineBuilder는 일반적으로 코루틴 내부에서 호출되어야 하기 때문에 suspend 키워드가 있는 함수라고 해서 호출할 수 있는 것은 아닙니다.

 

📌 join : 코루틴 빌더인 launch는 Job 객체를 반환하며 이 Job 객체를 통해 이 Job을 대기, 취소 등의 핸들링이 가능합니다.

중요한 점은 join()이 호출된 시점부터 해당 Job 객체에 대한 처리를 기다린 후 아래 코드를 실행한다는 것 즉, join()이 호출되는 위치에 따라 결과가 달라질 수 있습니다.

 

⑥ 실습

각 메서드에 0.01초간 대기하도록 구현해 exam()의 코드가 먼저 실행됩니다.

class MainViewModel : ViewModel() {


    suspend fun runLaunch1() {
        delay(100)
        Log.d("VM","launch1: ${Thread.currentThread().name}")
        Log.d("VM", "1")
    }

    suspend fun runLaunch2() {
        delay(100)
        Log.d("VM","launch2: ${Thread.currentThread().name}")
        Log.d("VM", "2")
    }

    suspend fun runLaunch3() {
        delay(100)
        Log.d("VM","launch3: ${Thread.currentThread().name}")
        Log.d("VM", "3")
    }


    fun exam() {
        viewModelScope.launch {

            launch {
                runLaunch2()
            }

            launch {
                runLaunch3()
            }

            runLaunch1()
        }

        Log.d("VM", "exam: ${Thread.currentThread().name}")
        Log.d("VM", "0")
    }
}
실행 결과
exam: main
0
launch1: main
1
launch2: main
2
launch3: main
3

 

⑦ 실습

각 메서드에 0.01초간 대기하도록 구현해 exam()의 코드가 먼저 실행되지만 join()에 의해 runLaunch1()이 가장 먼저 호출되지 않고 runLaunch2(), runLaunch3()가 호출된 후 runLaunch1()이 호출됩니다.

class MainViewModel : ViewModel() {


    suspend fun runLaunch1() {
        delay(100)
        Log.d("VM","launch1: ${Thread.currentThread().name}")
        Log.d("VM", "1")
    }

    suspend fun runLaunch2() {
        delay(100)
        Log.d("VM","launch2: ${Thread.currentThread().name}")
        Log.d("VM", "2")
    }

    suspend fun runLaunch3() {
        delay(100)
        Log.d("VM","launch3: ${Thread.currentThread().name}")
        Log.d("VM", "3")
    }


    fun exam() {
        viewModelScope.launch {

            launch {
                runLaunch2()
            }

            launch {
                runLaunch3()
            }.join()

            runLaunch1()
        }

        Log.d("VM", "exam: ${Thread.currentThread().name}")
        Log.d("VM", "0")
    }
}
실행 결과
exam: main
0
launch2: main
2
launch3: main
3
launch1: main
1