[Kotlin] 코루틴의 실행 순서와 테스트 코드
비동기 코드는 본질적으로 어렵다. 제대로 공부하지 않은 비동기 코드는 더 어렵다.
지난 몇 일간 코루틴 ``ViewModel`` 코드를 테스트하다 아주 열불이 나서..;; 이참에 제대로 다시 공부해 보자.
``suspend`` 함수의 실행 순서
``suspend`` 함수도 일반 함수처럼 순차적으로 실행된다.
suspend fun function1() {
println("fun 1")
}
suspend fun function2() {
println("fun 2")
}
fun main() = runBlocking {
function1()
function3()
}
fun 1
fun 2
일반 함수를 섞어도 똑같다.
suspend fun function1() {
println("fun 1")
}
suspend fun function2() {
println("fun 2")
}
suspend fun function3() {
println("fun 3")
}
fun main() = runBlocking {
function1()
function2()
function3()
}
fun 1
fun 2
fun 3
여기까지는 매우 직관적이다. 그럼 뭐가 문제인가?
``launch``를 실행하면?
나의 테스트 코드는 대략 다음과 같다.
fun testSomething() = runTest {
viewModel.insertData(data)
}
// ViewModel
suspend fun insertData(data: Data) {
viewModelScope.launch {
database.insert(data)
}
}
함수 안에서 ``launch``를 이용해 새로운 coroutine scope를 만들고 있다. 위의 코드처럼 명시적으로 만들어진 코루틴은 비동기적으로 실행된다. 그러니까 원래의 코루틴과 새로 만들어진 코루틴이 비동기적으로 실행된다는 의미이다.
비동기 작업이라는 본질에는 더 가까운 코드이지만, 테스트 환경에서는 순차적인 실행이 필요한데. 어떻게 해야 저 코루틴들을 순차적으로 실행할 수 있을까?
테스트 환경
suspend fun function1() {
lanunch {
println("fun 1 start")
delay(1000)
println("fun 1 end")
}
}
suspend fun function2() {
launch {
println("fun 2 start")
delay(1000)
println("fun 2 end")
}
}
fun main() = runBlocking {
function1()
function2()
}
fun 1 start
fun 2 start
fun 1 end
fun 2 end
``launch``로 별도의 코루틴 범위 2개를 만들었더니 두 함수가 비동기적으로(동시에) 실행된다. 나의 목표는 다음과 같이 두 함수를 동기적으로 실행하는 것이다.
fun 1 start
fun 1 end
fun 2 start
fun 2 end
사실 간단하다
다음의 코드를 보자.
suspend fun function1() = launch {
println("fun 1 start")
delay(1000)
println("fun 1 end")
}
suspend fun function2() = launch {
println("fun 2 start")
delay(1000)
println("fun 2 end")
}
fun main() = runBlocking {
function1().join()
function2().join()
}
각 함수가 ``Job``을 반환하고, ``Job.join()`` 메소드를 호출하여 각 코루틴이 종료될 때까지 기다린다. 명시적으로 만들어진 동시성은 명시적으로 제어하면 된다.
fun 1 start
fun 1 end
fun 2 start
fun 2 end
원하는 결과를 얻었다.
코루틴이 더 복잡하다면?
``launch``는 현재 스레드를 block하지 않는 새로운 코루틴을 실행한다. 복잡한 콜 스택 사이에서 ``Job`` 객체를 얻기도 어렵고, 그렇다고 강제로 block할 수도 없고..
``withContext`` 메소드를 사용하여 특정 코드블럭이 끝날 때까지 기다릴 수 있다. 이걸 어떻게 잘 쓰면 되지 않을까?
Calls the specified suspending block with a given coroutine context, suspends until it completes, and returns the result.
suspend fun function1() = launch {
println("fun 1 start")
delay(1000)
println("fun 1 end")
}
suspend fun function2() = launch {
println("fun 2 start")
delay(1000)
println("fun 2 end")
}
fun main() = runBlocking {
withContext(this.coroutineContext) {
function1()
}
withContext(this.coroutineContext) {
function2()
}
}
fun 1 start
fun 1 end
fun 2 start
fun 2 end
원하는 결과를 얻을 수 있다. 더 확실한 결과를 얻고 싶다면 ``withContext``의 맨 마지막에 ``delay``를 넣어도 된다.
결론
``withContext``를 ``delay``와 함께 사용하자. 물론 이 방법은 코루틴의 비동기성을 부정하는 방법이므로 테스트 환경 등 특수한 상황에서만 사용하자.