나미래 Android 개발자

[Android Coroutine] async 제대로 이해하기 — 반환값 있는 비동기 작업의 정석 본문

안드로이드/Coroutine

[Android Coroutine] async 제대로 이해하기 — 반환값 있는 비동기 작업의 정석

Moimeme Futur 2025. 9. 7. 11:35

이 글은 마르친 모스카와의 ⟪코틀린 코루틴⟫ 책을 기반으로 작성하였습니다.

 

여러분은 코루틴을 사용하기 위해 어떤 방식으로 코루틴을 만들고 계신가요?

아마 비동기 처리를 효과적으로 하기 위해서 직접 스레드를 생성하지 않고, 자연스럽게 중단(suspend) 함수를 호출하면서 코루틴에서 처리하도록 하고 있을 겁니다.

그렇다면 질문 하나 드려볼게요.

 

코루틴을 생성할 때, 어떤 상황에서 어떤 코루틴 빌더를 사용해야 하는지 설명할 수 있으신가요?

 

여기서 등장하는 개념이 바로 코루틴 빌더(Coroutine Builder) 입니다.

이번 글에서는 Kotlinx.coroutines 라이브러리에서 제공하는 코루틴 빌더 중, async 함수를 집중적으로 살펴보겠습니다.

 

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

 

 

문제 상황(요구사항)

먼저 async 함수를 이해하기 위해서 간단한 개발 요구사항을 이야기하겠습니다.

만약 여러분이 다음과 같은 상황에서 어떻게 개발을 할 것인지 생각해보면서 읽으시면 코루틴 빌더 async 함수를 더 깊게 이해하고 오랫동안 기억하실 수 있으실 겁니다.

 

스레드를 멈추지 않고 여러 개의 엔드포인트에서 데이터를 동시에 얻어야 합니다.

그리고 얻어온 데이터를 합쳐서 하나의 결과 값을 만들어 반환해야 합니다.

 

위와 같은 경우 여러분이라면 어떻게 코루틴을 생성해서 비동기를 효과적으로 처리하실 건가요?

여기서 주요 요구사항은 다음과 같습니다.

  1. 코루틴을 이용해 여러 개의 엔드포인트에서 데이터를 동시에 얻는다.
  2. 여러 곳에서 얻어온 데이터를 합쳐야 한다.
  3. 데이터를 합쳐서 만들어진 새로운 데이터를 반환해야 한다.

 

예를 들어 인스타그램 앱에서 프로필 화면에 진입했을 때, 사용자 프로필 정보, 게시물 수, 팔로워 수, 팔로잉 수를 각각 다른 엔드포인트에서 동시에 가져와 합쳐야 하는 상황을 떠올리면 이해하기 쉽습니다.

 

 

해결 방안 아이디어

데이터를 동시에 받아오기 위해서 4가지 데이터(사용자 프로필 정보, 게시물 수, 팔로워 수, 팔로우 수)를 받아오는 API를 호출하는 코루틴을 각각 만들어서 사용해야 합니다.

그리고 각 데이터를 합쳐서 유저 데이터를 만들어야 하기 때문에 각각의 코루틴에서 데이터를 받아오고 난 후, 유저 데이터를 만들어야 합니다.

이런 경우 가장 적절한 코루틴 빌더가 바로 async 입니다.

 

 

async 함수의 특성

fun <T> CoroutineScope.async {
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
}: Deferred<T>
  1. CoroutineScope 확장 함수
    • CoroutineScope를 리시버로 받기 때문에, async를 호출하려면 CoroutineScope가 필요합니다.
    • 예: lifecycleScope.async { … }, viewModelScope.async { … }, GlobalScope.async { … }
  2. suspend 고차 함수를 블록으로 받음
    • block 파라미터에 넘기는 람다 내부는 중단 가능합니다.
    • 즉, delay, withContext, suspend fun 등을 자연스럽게 호출할 수 있습니다.
  3. Deferred 반환
    • async는 Deferred를 반환합니다.
    • Deferred는 Job을 상속하면서도 결과 값을 await()으로 받아올 수 있습니다.
    • await()은 중단 함수이며, block의 마지막 라인 값이 반환됩니다.
    interface Deferred<out T> : Job {
        public suspend fun await(): T
    }

async 함수의 핵심 특징

  1. CoroutineScope 확장성
    • 부모 스코프의 생명주기를 자동으로 따르며, 부모가 취소되면 자식도 함께 취소됩니다.
  2. 결과 반환 중심
    • launch와 달리 async는 값 반환에 초점이 맞춰저 있습니다.
    • 반환값은 await()으로 호출부에서 안전하게 받을 수 있습니다.
  3. Deferred와 예외 전파
    • async가 반환하는 Deferred는 Job을 상속하므로 취소, 대기(Join), 상태 확인이 가능합니다.
    • 예외는 실행 시점이 아니라 await() 호출 시점에 호출부로 전파됩니다.

 

 

실제 사용 예시

간단한 콘솔 예시

fun main() = runBlocking {
    val res1 = GlobalScope.async {
        delay(1000L)
        "Text 1"
    }
    val res2 = GlobalScope.async {
        delay(3000L)
        "Text2"
    }
    val res3 = GlobalScope.async {
        delay(2000L)
        "Text3"
    }

    println(res1.await())
    println(res2.await())
    println(res3.await())
}
// [출력 결과]
// (1초 후)
// Text 1
// (2초 후)
// Text 2
// Text 3

 

여기서 async는 비동기 코루틴 실행을 담당합니다.

반환값이 있고 await() 중단 함수를 통해서 값을 받아오기 때문에 “Text 3”는 “Text 2”가 출력된 이후에 값을 받아와서 출력됩니다.

 

⚠️주의

GlobalScope는 구조적 동시성이 깨지므로 실무에서는 사용하지 않는 것을 강력히 권장합니다.

예제에서만 편의상 사용했습니다.

실제 앱 개발에서는 viewModelScope, lifecycleScope 등을 사용하는 것을 권장합니다.

 

 

Android 실무 예시: 인스타그램 유저 데이터 만들기

data class UserData(
    val userProfile: UserProfileData?,
    val posts: PostsData?,
    val follows: FollowsData?,
    val followers: FollowersData?
)

suspend fun getUserProfile(): UserData = coroutineScope {
    val userProfileDeferred = async { getUserProfileData() }
    val postsDeferred = async { getUserPosts() }
    val followsDeferred = async { getUserFollows() }
    val followersDeferred = async { getUserFollowers() }

    val userProfile = try { userProfileDeferred.await() } catch (e: Exception) { null }
    val posts = try { postsDeferred.await() } catch (e: Exception) { null }
    val follows = try { followsDeferred.await() } catch (e: Exception) { null }
    val followers = try { followersDeferred.await() } catch (e: Exception) { null }

    UserData(userProfile, posts, follows, followers)
}

 

어떠신가요? 위 코드가 어떻게 요구사항을 충족하는지 머릿속에 그려지시나요?

이 코드에서는 4개의 API 호출이 동시에 처리되고, 모든 결과가 완료되면 UserData 객체를 생성해서 반환합니다.

 

 

정리

async의 특징

  • CoroutineScope 확장 함수로, 부모 스코프의 생명주기를 따름
  • Deferred<T>를 반환하여 await()으로 결과를 가져올 수 있음
  • 예외는 실행 시점이 아니라 await() 호풀 시점에 호출부로 전파됨

언제 사용해야 할까?

  • 여러 API를 병렬로 호출한 뒤 결과를 합쳐야 할 때
  • 비동기 작업의 값 반환이 필요한 경우
  • UI에 표시할 데이터를 동시에 불러와 하나의 모델로 묶어야 할 때

주의사항

  • 반드시 await() 또는 awaitAll()로 결과를 소비해야 함
  • 예외 처리를 위해 각 await()을 try-catch로 감싸는 습관 필요
  • GlobalScope는 지양하고, lifecycleScope, viewModelScope 등 생명주기에 맞는 스포크 사용 권장

 

 

이제 “async 함수는 언제 사용하나요?” 또는 “비동기 작업을 동시에 처리하고 결과를 모아야 할 떄 어떻게 구현하나요?”라는 질문을 받아도 자신 있게 답할 수 있을 것입니다.

Comments