Primary/Kotlin

[Kotlin] 코루틴의 실행 순서와 테스트 코드

해스끼 2022. 4. 26. 20:19

비동기 코드는 본질적으로 어렵다. 제대로 공부하지 않은 비동기 코드는 더 어렵다. 

 

지난 몇 일간 코루틴 ``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``와 함께 사용하자. 물론 이 방법은 코루틴의 비동기성을 부정하는 방법이므로 테스트 환경 등 특수한 상황에서만 사용하자.