본문 바로가기
Android/Coroutine

[Android] 중단(suspend)함수를 비동기적으로 실행하는 방법도 있을까?

by 태크민 2025. 2. 5.

하나의 코루틴 내에서 중단 함수(suspend)를 호출하면 기본적으로 순차적으로 실행됩니다. 이는 일반적인 함수 호출과 유사하게, 앞선 함수가 완료된 후에 다음 코드가 실행되기 때문입니다.

 

개발을 하다 보면 네트워크를 통해 API를 호출할 때가 많습니다. 이때 상황에 따라 두 가지 방식으로 결과를 처리할 수 있습니다:

  1. 동기적으로 결과를 받아 처리해야 하는 경우
  2. 비동기적으로 여러 요청을 동시에 처리해야 하는 경우

로그인 요청처럼 이전 결과가 다음 로직에 영향을 주는 경우 순차적인 실행이 필요하지만,

2번과 같이 여러개의 API를 동시에 호출하여 전체 처리 시간을 줄이고 싶을 때는 비동기 실행이 더 효율적입니다.

 

이번 포스팅에서는 어떻게 하면 suspend함수를 비동기적으로 처리할 수 있을지에 대해 알아보도록 하겠습니다.

 

기본 순차 실행

두 개의 중단 함수가 다음과 같은 이름으로 정의되어 있다고 가정해 봅시다 (doSomethingUsefulOne, doSomethingUsefulTwo)

fun main(args: Array<String>) = runBlocking<Unit> {
    val time = measureTimeMillis {
        val one = doSomethingUsefulOne()
        val two = doSomethingUsefulTwo()
        println("The answer is ${one + two}")
    }
    println("Completed in $time ms")
}

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L)
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L)
    return 29
}

 

<결과>

The answer is 42
Completed in 2026 ms

 

위 코드를 실행하면, doSomethingUsefulOne() -> doSomethingUsefulTwo() -> print 순으로 실행됩니다.

결국 순차적으로 실행이 되는 것이죠.

총 소요시간을 보면 함수가 순차적으로 수행되었기 때문에, 거의 중단 함수 각각의 수행시간의 합 만큼의 시간이 소모 되었음을 알 수 있습니다.

 

 

aync 를 이용한 동시 수행 (Concurrent using async)

만약 앞서 예로 들었던 두 중단함수가 서로 의존성이 없고 둘중 어느것으로 부터든 빠른 쪽의 결과를 먼저 수신 하고자 한다면 async { } 빌더를 사용할 수 있습니다.

async { } 코루틴 빌더는 launch { } 빌더와 동일하게 전달된 중단함수 블록을 이용하여 새로운 코루틴을 생성 및 실행하지만 중단함수 블록의 실행 결과를 Deferred<T> 를 통해 전달 받을 수 있다는 점에서 차이가 있습니다. 

 

fun main(args: Array<String>) = runBlocking<Unit> {
    val time = measureTimeMillis {
        val one = async { doSomethingUsefulOne() }
        val two = async { doSomethingUsefulTwo() }
        println("The answer is ${one.await() + two.await()}")
    }
    println("Completed in $time ms")
}

 

<결과>

The answer is 42
Completed in 1072ms

 

이것은 async 를 사용하지 않은 경우에 비해서 2배 빠른데 그 이유는 두 코루틴을 동시에 수행했기 때문입니다. 코루틴 프레임워크에서는 순차 실행이 기본이기 때문에 동시 수행은 항상 명시적 이어야 합니다.

 

async는 이러한 동시 실행을 가능하게 해주는 핵심 기능입니다. async를 호출하면 새로운 코루틴이 즉시 시작되며, 결과를 기다릴 필요 없이 바로 Deferred 객체를 반환합니다.

Deferred 객체비동기 작업의 결과를 나중에 받아올 수 있는 일종의 약속(Promise) 역할을 합니다.

실제 결과가 필요한 시점에 await()를 호출하면, 해당 결과가 준비될 때까지 대기하게 됩니다.

(여기서 결과는 async의 마지막 line입니다.)

 

따라서, async가 호출되는 순간 두 코루틴이 동시에 비동기적으로 실행되기 때문에, 각각 1초가 걸리는 작업이 2초가 아닌 1초 만에 완료되는 것입니다.

 

async 의 지연 실행 (Lazily started async)

async 는 start 파라미터에 CoroutineStart.LAZY 값을 전달하여 실행을 지연시킬 수 있습니다.

이 옵션이 적용된 async 코루틴은 결과 값이 필요한 시점에 await() 이나 start() 함수가 호출되는 시점에 시작됩니다.

fun main(args: Array<String>) = runBlocking<Unit> {
    val time = measureTimeMillis {
        val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
        val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }

        //If comment out below two lines, two coroutines will be called sequentially.
        one.start()
        two.start()

        println("The answer is ${one.await() + two.await()}")
    }
    println("Completed in $time ms")
}

 

위 예제를 보면 두 개의 코루틴을 선언 하면서 실행 옵션은 CoroutineStart.LAZY 를 전달하여 실행 시점을 값이 필요한 시점으로 변경하고 있습니다. 

이 경우 asnyc는 즉시 실행되지 않으며, 개발자가 원하는 시점에 start() 함수를 호출하여 해당 코루틴을 실행해여야 합니다. 

위 예제에서는 두개의 코루틴을 시작시키고 그 결과를 출력하기 위해서 출력문에서 대기 하게됩니다.

 

결과는 어떻게 나올까요?

The answer is 42
Completed in 1072ms

 

start()를 하여 2개의 작업을 비동기로 실행하였기 때문에, Lazy를 사용하지 않은 경우와 마찬가지로 1초가 나왔습니다.

 

하지만, 만약 one.start(), two.start() 두 라인을 주석처리하여 시작시키지 않는다면 어떻게될까요?

fun main(args: Array<String>) = runBlocking<Unit> {
    val time = measureTimeMillis {
        val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
        val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }

        //If comment out below two lines, two coroutines will be called sequentially.
        //one.start()
        //two.start()

        println("The answer is ${one.await() + two.await()}")
    }
    println("Completed in $time ms")
}

 

<결과>

The answer is 42
Completed in 2072ms

 

출력문의 one.await() 이 해당 코루틴을 시작시키고 그 결과를 받고 나서 다음 two.await() 이 호출되기 때문에 결국 순차적으로 수행했을때와 같은 2초정도의 시간이 걸리게됩니다.

 

 

launch를 수행하여도 비동기로 실행시킬 수 있지 않나요?

launch와 join을 조합하면, 코루틴을 비동기적으로 실행한 후 완료될 때까지 대기할 수 있습니다.

하지만 launch는 단순히 작업을 실행하기 위한 목적으로 설계되어 있으며, 결과 값을 반환하지 않습니다.

  • 단순히 비동기 작업을 실행하고 결과가 필요 없는 경우 → launch 사용
  • 비동기 작업의 결과가 필요한 경우 → async 사용
fun main(args: Array<String>) = runBlocking<Unit> {
    val time = measureTimeMillis {
        val one = launch { doSomethingUsefulOne() }
        val two = launch { doSomethingUsefulTwo() }

        joinAll(one,two)
    }
    println("Completed in $time ms")
}

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L)
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L)
    return 29
}

 

<결과>

Completed in 1051 ms

참고자료

https://myungpyo.medium.com/%EC%BD%94%EB%A3%A8%ED%8B%B4-%EA%B3%B5%EC%8B%9D-%EA%B0%80%EC%9D%B4%EB%93%9C-%EC%9E%90%EC%84%B8%ED%9E%88-%EC%9D%BD%EA%B8%B0-part-4-70b0c0fb492

 

코루틴 공식 가이드 자세히 읽기 — Part 4

공식 가이드 읽기 (4 / 8)

myungpyo.medium.com