안드로이드 Coroutine의 job과 Deferred의 차이란? (3)

쓰리원·2022년 6월 6일
0

Coroutine 정리

목록 보기
4/4
post-thumbnail

1. 들어가기 전에

코루틴의 비동기 실행은 launch와 async로 할 수 있습니다. 동작은 비슷하게 하지만 둘의 차이가 있습니다.

  • launch - 새로운 코루틴을 시작. 결과값을 전달하지 않을 때 사용.
  • async - 결과를 리턴할 수 있는 코루틴을 시작할때만 사용.

둘의 차이는 리턴값의 차이라고 볼 수 있습니다. launch를 사용하면 job 을 반환하고 async를 사용하면 Deferred를 반환하게 됩니다. 둘중 어느것을 사용하는가에 메서드나 완료 대기에 대한 대응이 달라집니다.

Deferred는 job을 확장하는 인터페이스로 만들져서 Deferred는 job 의 모든 특성을 가지고 있습니다. async를 사용하고 결과값을 수신하기 위해 await()를 사용하면 Deferred를 반환하게 됩니다.

2. launch() : Job

launch() 함수로 시작된 코루틴 블록은 Job 객체를 반환합니다.

val job : Job = launch {
    ...
}

반환받은 Job 객체로 코루틴 블록을 취소하거나, 다음 작업의 수행전 코루틴 블록이 완료 되기를 기다릴수 있습니다.

        val job = launch {
            var i = 0
            while (i < 10) {
                delay(1000)
                i++
            }
        }

        job.join() // 완료 대기
        job.cancel() // 취소

여러개의 launch 코루틴 블록을 실행할 경우 각각의 Job 객체에 대해서 join() 함수로 코루틴 블록이 완료 될때까지 다음 코드 수행을 대기할수 있습니다.

 val job1 : Job = launch {
            var i = 0
            while (i < 10) {
                delay(1000)
                i++
            }
        }

        val job2 = launch {
            var i = 0
            while (i < 10) {
                delay(1000)
                i++
            }
        }

        job1.join()
        job2.join()
        
        //아래와 같이도 가능
        joinAll(job1, job2)

모든 Job 객체에 대해서 join() 함수를 호출하지 않고 joinAll() 함수로 모든 launch 코루틴 블록이 완료 되기 기다릴 수도 있습니다.

      val job1 = launch {
            var i = 0
            while (i < 10) {
                delay(1000)
                i++
            }
        }

        // 위 블록 과 같은 job1 객체를 사용
        launch(job1) {
            var i = 0
            while (i < 10) {
                delay(1000)
                i++
            }
        }

        // 같은 job 객체를 사용하게 되면
        // joinAll(job1, job2) 와 같다
        job1.join()

위의 예시와 같이 첫번째 launch 코루틴 블록에서 반환받은 Job 객체를 두번째 launch() 함수의 인자로 사용하면, 동일한 Job 객체로 두개의 코루틴 블록을 모두 제어 할수 있습니다. launch() 함수로 정의된 코루틴 블록은 즉시 수행되며, 반환 받은 Job 객체는 해당 블록인 launch()의 제어만 가능합니다.

3. async() : Deferred

코루틴 블록의 결과 값을 반환을 위해서는 async() 코루틴 블록을 생성합니다. async() 함수로 시작된 코루틴 블록은 Deferred 객체를 반환합니다.

val deferred : Deferred<T> = async {
    ...
    T // 결과값
}

위 코루틴 블록은 Deferred 객체를 이용해 제어가 가능하며 동시에 코루틴 블록에서 계산된 결과값을 반환 받을수 있습니다.

        val deferred : Deferred<String> = async {
            var i = 0
            while (i < 10) {
                delay(1000)
                i++
            }
            "result"
        }
        
        val msg = deferred.await()
        println(msg) // result 출력

여러개의 async 코루틴 블록을 실행할 경우 각각의 Deferred 객체에 대해서 await() 함수로 코루틴 블록이 완료 될때까지 다음 코드 수행을 대기할수 있습니다. await() 함수는 코루틴 블록이 완료되면 결과를 반환합니다.

        val deferred1 = async {
            var i = 0
            while (i < 10) {
                delay(1000)
                i++
            }
            "result1"
        }

        val deferred2 = async {
            var i = 0
            while (i < 10) {
                delay(1000)
                i++
            }
            "result2"
        }

        val result1 = deferred1.await()
        val result2 = deferred2.await()
        
        awaitAll(deferred1, deferred2)
        
        println("$result1 , $result2") // result1 , result2 출력

각각의 Deferred 객체에 대해서 await() 함수를 호출하지 않고 awaitAll() 함수를 이용하여 모든 async 코루틴 블록이 완료 되기를 기다릴수도 있습니다.

  val deferred = async {
            var i = 0
            while (i < 10) {
                delay(1000)
                i++
            }
            "result1"
        }

        // 같은 Deferred 객체 사용
        async(deferred) {
            var i = 0
            while (i < 10) {
                delay(1000)
                i++
            }
            "result2"
        }

        val msg = deferred.await()
        println(msg) // 첫번째 블록 결과인 result1 출력

위의 예시와 같이 첫번째 async 코루틴 블록에서 반환받은 Deferred 객체를 두번째 async() 함수의 인자로 사용하면, 동일한 Deferred 객체로 두개의 코루틴 블록을 모두 제어 할수 있습니다.

단, 여러개의 async 코루틴 블록에 같은 Deferred 객체를 사용할경우 await() 함수 호출시 전달되는 최종적인 결과값은 첫번째 async 코루틴 블록의 결과값 만을 전달한다는것에 주의해야 합니다.

4. job과 Deferred의 차이

예외가 자동으로 전파되는 job과는 달리 Deferred는 예외를 자동으로 전파하지 않습니다. 이는 Deferred가 결과값 수신을 대기해야 하기 때문입니다.

suspend fun main() {

	val exceptionHandler = CoroutineExceptionHandler { _ , exception ->
    	when(exception) {
        	is IllegalArgumentException -> println("error1")
            is InterruptedExcetpion -> println("error2")
        }
    }
    
    val deferred = CoruntineScope(Dispatcher.IO).async(exceptionHandler) {
    	throw IllegalArgumentException()
        arrayOf(1,2,3)
    }
    delay(1000)
}

이 코드를 실행하면 뭐라도 실행창에 나올 것 같지만 아무것도 나오지 않습니다. 왜냐하면 위에 말한 것 처럼 Deferred는 자동으로 에러를 전파하지 않기 때문입니다.

Deferred는 await()를 사용해서 미래 시점에 값을 받는데 해당 값이 필요없는 상황에서는 에러를 전파 시킬 필요가 없기 때문입니다. 위 코드에 delay 부분을 지우고 deferred.await()로 바꾸면 이제는 해당 값이 필요하게 됐으므로 에러를 발생시키게 됩니다.

suspend fun main() {

	val exceptionHandler = CoroutineExceptionHandler { _ , exception ->
    	when(exception) {
        	is IllegalArgumentException -> println("error1")
            is InterruptedExcetpion -> println("error2")
        }
    }
    
    val deferred = CoruntineScope(Dispatcher.IO).async(exceptionHandler) {
    	throw IllegalArgumentException()
        arrayOf(1,2,3)
    }
    deferred.await()
}

하지만 위 deferred.await()를 사용하여 실행한 코드는 문제가 있습니다. 에러를 처리할때 핸들러에서 처리하는게 아니라 main 스레드에서 에러가 전파되어서 main 스레드가 종료되어버리는 문제가 있습니다.

suspend fun main() {

	val exceptionHandler = CoroutineExceptionHandler { _ , exception ->
    	when(exception) {
        	is IllegalArgumentException -> println("error1")
            is InterruptedExcetpion -> println("error2")
        }
    }
    
    val deferred = CoruntineScope(Dispatcher.IO).async {
    	throw IllegalArgumentException()
        arrayOf(1,2,3)
    }
    
    CoruntineScope(Dispatcher.IO).launch(exceptionHandler) {
    	 deferred.await()
    }.join()
}

이렇게 deferred.await()로 에러가 전파될 수 있는 위치에 핸들러를 넣어주면 main 스레드가 종료되지 않고 에러 핸들링을 하게 됩니다.

즉, launch() 함수로 정의된 코루틴 블록은 즉시 수행되며, 반환 받은 Job 객체는 해당 블록을 제어는 할수 있지만 코루틴 블록의 결과를 반환하지는 않습니다.

코루틴 블록의 결과 값을 반환받고 싶다면 async() 코루틴 블록을 생성합니다. job객체와 동일하게 블록제어도 가능합니다. 그리고 await()로 해당 값을 받을 수 있습니다. 이러한 결과값을 받고 안받고의 차이로 인해 에러가 전파되는 방식이 다르게 됩니다.

5. reference

https://developer.android.com/kotlin/coroutines/coroutines-adv?hl=ko
https://kotlinlang.org/docs/coroutines-basics.html#an-explicit-job

profile
가장 아름다운 정답은 서로의 협업안에 있다.

0개의 댓글