async: 것이 뭐인가, Deferred 및 병렬 코루틴이 작동하는 방식

저자: IT Sectr 게시일: 2026-06-22 읽는 시간: 8 분

async — Kotlin의 Coroutine Builder로, 코루틴을 시작하고 비동기 작업의 결과를 얻는 데 Deferred<T> 객체를 반환합니다. async는 여러 태스크를 병렬로 실행하고 await()를 통해 결과를 집계할 수 있습니다. JetBrains 가이드, 2024에 따르면, async는 코루틴에서 병렬 계산을 조직하는 가장 바랄직한 방법입니다.

주요 포인트

  • async — 비동기 작업의 결과가 담긴 Deferred<T>를 반환하는 코루틴 빌더
  • Deferred — Job을 상속받으며, 결과를 얻기 위한 await() 메소드를 가진 비동기 프로미스
  • async는 독립적인 태스크를 병렬로 실행하여 전체 시간을 줄인다
  • await() — 블로킹 없이 결과가 나올 때까지 코루틴을 일시 중단하는 suspend 함수
  • 구조화된 동시성을 async와 함께 사용하면 오류 시 모든 병렬 태스크가 취소됨을 보장한다

Kotlin Coroutines에서 async란?

async — CoroutineScope의 확장 함수로, 결과를 반환하는 코루틴을 만듭니다. launch와 달리, async는 Deferred<T> — 미래 값을 나타내는 객체를 반환합니다. async로 시작된 코루틴은 다른 코루틴과 동시에 실행되어 계산을 병렬화할 수 있습니다.

async는 비동기 작업의 결과가 후속 처리에 필요할 때 사용됩니다: 여러 소스에서 데이터 로드, 커렉션의 일괄 처리, 여러 API에 대한 병렬 요청 등이 있습니다. Deferred는 즉시 생성되며, 코루틴이 완료된 후에 결과를 사용할 수 있습니다.

kotlin
import kotlinx.coroutines.*

suspend fun String.delayedValue(): String {
    delay(1000L)
    return this
}

fun main() = runBlocking {
    val deferred: Deferred<String> = async { "Hello".delayedValue() }
    println(deferred.await())
}

async와 Deferred 구문

async의 시그니처는 launch와 유사합니다: 동일한 CoroutineContext, CoroutineStart 매개변수 및 suspend 블록입니다. 차이점은 반환 유형에 있습니다 — Job 대신 Deferred<T>를 반환하며, 여기서 T는 블록의 마지막 표현식이 반환하는 결과 유형입니다.

kotlin
public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T>

Deferred<T> — Job을 상속받고 await(), getCompleted(), getCompletionExceptionOrNull() 메소드를 추가하는 인터페이스입니다. await()은 suspend 함수로, 코루틴 또는 다른 suspend 함수 내에서만 호출할 수 있습니다. getCompleted()는 블로킹되지 않는 메소드로, 코루틴이 아직 완료되지 않았으면 예외를 발생시킵니다.

async로 병렬 태스크

async의 주요 장점은 태스크를 병렬로 실행할 수 있다는 점입니다. 각 작업을 순차적으로 기다리는 대신, async는 코루틴을 동시에 시작하여 전체 시간을 가장 긴 작업의 기간으로 줄입니다.

병렬 데이터 로드

일반적인 시나리오 — 세 개의 병렬 async 요청으로 사용자 프로필, 설정 및 주문 내역을 로드하는 것입니다. 세 개 모두 완료된 후 결과는 하나의 데이터 모델로 결합됩니다.

kotlin
suspend fun loadUserDashboard(userId: String): Dashboard = coroutineScope {
    val profile = async { api.getProfile(userId) }
    val settings = async { api.getSettings(userId) }
    val orders = async { api.getRecentOrders(userId) }

    Dashboard(
        profile = profile.await(),
        settings = settings.await(),
        orders = orders.await()
    )
}

coroutineScope 함수는 하위 코루틴을 시작하고 결과를 반환하기 전에 완료를 기다립니다. 전체 블록은 순차적으로 아니라 병렬로 실행됩니다.

Deferred 결과 처리

Deferred는 결과를 다루기 위한 여러 메소드를 지원합니다. await() 외에도 상태 확인, 완료된 값 획득 및 Kotlin Coroutines 유틸리티 함수를 통한 여러 Deferred 집계 방법이 있습니다.

메소드설명Suspend?
await()결과가 나올 때까지 코루틴을 일시 중단한다
getCompleted()코루틴이 완료되면 결과 반환 (그렇지 않으면 Exception)아니오
getCompletionExceptionOrNull()오류 시 예외 또는 null 반환아니오
isCompleted코루틴이 완료되었는지 확인한다아니오

Deferred 목록을 집계하기 위해 awaitAll()이 사용됩니다 — 컬렉션 내의 모든 Deferred가 완료될 때까지 실행을 일시 중단하는 함수입니다. 하나라도 오류로 완료되면 awaitAll()이 예외를 발생시킵니다.

kotlin
suspend fun <T> loadAll(requests: List<suspend () -> T>): List<T> {
    return coroutineScope {
        val deferreds = requests.map { async { it() } }
        deferreds.awaitAll()
    }
}

async와 launch의 차이점

async와 launch 사이의 선택은 코루틴 결과가 필요한지에 따라 달라집니다. launch는 Job을 반환하고 fire-and-forget 작업에 적합하며, async는 Deferred를 반환하고 데이터를 반환하는 태스크에 사용됩니다. 구문과 매개변수 측면에서 빌더는 동일합니다.

  • launch → Job (결과 없음), async → Deferred<T> (결과 T)
  • launch — fire-and-forget 태스크 (로깅, 캐싱, 분석 보내기)
  • async — 병렬 계산 (여러 API에서 로드, 일괄 처리)
  • 둘 모두 구조화된 동시성 및 CoroutineStart 매개변수를 지원
  • await()은 Deferred에서만 사용 가능, launch는 결과 제공 안함

중요한 규칙: fire-and-forget에 async를 사용하지 마세요. 코루틴 결과가 필요 없다면 launch를 사용하세요. async는 Deferred를 생성하여 오버헤드를 만들며, 이런 시나리오에서는 필요 없습니다.

모바일 개발에서 async 예시

Android에서 async는 ViewModel 및 UseCases 내의 병렬 작업에 사용됩니다. 일반적인 시나리오: 모바일 개발에서의 예시, 여러 발전 작업이 있습니다.

kotlin
class HomeViewModel : ViewModel() {
    fun loadHomeScreen() {
        viewModelScope.launch {
            val userData = async(Dispatchers.IO) { repository.getUserData() }
            val newsFeed = async(Dispatchers.IO) { repository.getNewsFeed() }
            val notifications = async(Dispatchers.IO) { repository.getNotifications() }

            val state = HomeState(
                user = userData.await(),
                news = newsFeed.await(),
                unread = notifications.await()
            )
            _uiState.update { state }
        }
    }
}

오류 격리를 위해 async 그룹을 coroutineScope나 supervisorScope에 감싸는 것이 권장됩니다. supervisorScope는 하나가 실패해도 나머지 async 코루틴이 계속되도록 하여 비중요 작업에 유용합니다.

자주 묻는 질문

await 없이 async를 사용할 수 있나요?

가능하지만 의미가 없습니다. await() 없는 async는 코루틴을 시작하지만 결과가 손실됩니다. 결과가 필요 없으면 launch를 사용하세요 — Deferred를 만들지 않으며 메모리와 성능이 더 효율적입니다.

async는 예외를 어떻게 처리하나요?

async 내의 예외는 Deferred에 저장됩니다. await()을 호출하면 이 예외가 발생합니다. 발생 없이 확인하려면 getCompletionExceptionOrNull()을 사용하세요. coroutineScope를 사용할 때 오류는 모든 하위 코루틴을 취소합니다.

Kotlin Coroutines에서 awaitAll이란?

awaitAll() — Iterable<Deferred<T>>의 확장 함수로, 모든 Deferred가 완료될 때까지 코루틴을 일시 중단합니다. 결과 목록을 반환합니다. 하나라도 Deferred가 실패하면 예외를 발생시키고 나머지를 취소합니다.

async는 GlobalScope.async와 어떻게 다른가요?

그러합니다. async는 CoroutineScope의 확장 함수로 스코프에 역이 있습니다. GlobalScope.async는 수명 주기에 역이 없는 코루틴을 만들어 무한히 실행될 수 있습니다. Android에서는 메모리 누수 위험으로 GlobalScope가 권장되지 않습니다.

병렬 요청에 많은 async 코루틴을 시작할 수 있나요?

수는 디스패처에 따라 달라집니다: Dispatchers.IO는 최대 64개의 동시 스레드를 지원하고, Dispatchers.Default는 CPU 코어 수까지 지원합니다. 대규모 작업의 경우 Semaphore 또는 awaitAll과 함께 mapNotNull을 통해 제한된 병렬성을 사용하세요.

요약

  • async — 비동기 결과 획득을 위해 Deferred<T>를 반환하는 코루틴 빌더
  • Deferred는 Job을 상속받고 await(), getCompleted(), getCompletionExceptionOrNull() 메소드 추가
  • 병렬 실행으로 전체 시간이 가장 긴 작업의 기간으로 줄어들
  • awaitAll() — 컬렉션 내의 모든 Deferred를 기다리는 유틸리티
  • coroutineScope — 취소 보장이 있는 async 그룹에 권장되는 래퍼
  • async를 fire-and-forget에 사용하지 마세요 — 대신 launch를 사용하세요
  • Android에서 async는 viewModelScope와 함께 ViewModel에서 병렬 로드에 사용됩니다

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기