Async/Await는 동기 스타일로 비동기 코드를 작성하기 위한 키워드 쌍으로, Swift에서는 iOS 13부터, Kotlin에서는 코루틴과 함께 사용할 수 있습니다. Apple Swift Documentation, 2026에 따르면, Async/Await는 콜백 체인과 GCD를 대체하여 비동기 코드를 선형적이고 읽기 쉽게 만듭니다. async 키워드는 함수를 비동기로 표시하고, await는 결과를 받을 때까지 실행을 일시 중단합니다.
핵심 포인트
Async/Await는 동기 코드만큼 선형적으로 비동기 코드를 작성할 수 있게 해주는 언어 구조입니다. Swift에서는 iOS 13 / macOS 10.15에서 Swift Concurrency 프레임워크와 함께 등장했습니다. Kotlin에서는 버전 1.3부터 코루틴 라이브러리(kotlinx.coroutines)를 통해 async/await를 사용할 수 있습니다.
async/await 이전에는 비동기 코드가 콜백, GCD 또는 RxSwift를 기반으로 구축되었습니다. 중첩된 콜백 체인은 읽기와 디버깅을 어렵게 만드는 깊은 중첩인 콜백 지옥을 초래했습니다. Async/Await는 비동기 함수가 await 지점에서 “일시 중단”되고 결과를 받은 후 재개될 수 있게 하여 이 문제를 해결합니다.
Google(2025)에 따르면, Kotlin에서 async/await를 사용하면 콜백 방식과 비교하여 비동기 코드 줄 수가 40–60% 감소합니다. Swift에서는 2025년에 게시된 애플리케이션 중 Swift Concurrency 채택률이 68%에 도달했습니다. iOS 16은 UIKit, SwiftUI 및 Foundation에 Swift Concurrency 지원을 추가하여 async/await를 모든 새로운 Apple 프로젝트의 표준 도구로 만들었습니다.
async/await와 스레드의 주요 차이점은 차단 없는 일시 중단입니다. 함수가 await를 만나면 일시 중단되어 현재 스레드를 다른 작업에 해제합니다. 비동기 작업이 완료되면 함수는 동일하거나 다른 스레드에서 재개됩니다. 이것을 협력적 멀티태스킹이라고 합니다.
비동기 함수는 상태 머신(실행 상태를 관리하는 유한 오토마타)으로 컴파일됩니다. await 지점에서 컴파일러는 컨텍스트(지역 변수, 반환 주소)를 저장하고 제어를 런타임에 넘깁니다. 결과가 준비되면 런타임이 컨텍스트를 복원하고 실행을 계속합니다.
Swift에서 이 상태 머신은 SIL(Swift Intermediate Language) 컴파일러 수준에서 구현됩니다. Kotlin에서는 CPS(Continuation Passing Style) 메커니즘이 사용됩니다. 각 suspend 함수는 결과가 반환되는 숨겨진 Continuation 매개변수를 받습니다. 두 접근 방식 모두 메모리 누수 없이 안전한 재개를 보장합니다.
콜백 방식은 작업 완료 후 호출되는 클로저를 전달해야 합니다. 세 개의 순차적 요청이 있는 경우 각 수준에서 오류 처리가 포함된 세 단계의 중첩이 발생합니다. Async/Await는 동일한 세 요청을 단일 catch 블록이 있는 세 줄의 순차적 코드로 변환합니다.
GCD(DispatchQueue)는 직렬 큐와 DispatchGroup을 통해 콜백 지옥을 해결하지만 여전히 장황합니다. 간단한 작업(사용자 로드, 친구 로드, UI 로드)에는 큐, 그룹 및 notify 블록이 필요합니다. Async/Await는 단일 함수에서 세 줄로 동일한 작업을 수행합니다.
| 측면 | 콜백 | GCD | Async/Await |
|---|---|---|---|
| 중첩 | 깊음(파멸의 피라미드) | 중간(그룹+알림) | 선형(평면) |
| 오류 처리 | 콜백별 | 수동 | 통합(try/catch) |
| 취소 | 수동 | 제한적 | Task.cancel() |
| 스레드 안전성 | 수동 | 직렬 큐 통해 | MainActor 통해 |
Apple WWDC 2024에 따르면, Swift Concurrency async/await는 새 코드에 권장되는 접근 방식입니다. GCD는 C 라이브러리와의 통합 및 특정 저수준 스레드 처리 시나리오를 위해 남아 있습니다.
Task는 Swift에서 비동기 작업의 기본 단위입니다. Task는 기존 액터의 컨텍스트 내에서 또는 임의의 스레드에서 생성됩니다. Task 내에서는 await를 통해 비동기 함수를 호출할 수 있습니다. TaskGroup은 여러 하위 작업을 병렬로 시작하고 결과를 수집할 수 있습니다. 이는 DispatchGroup의 향상된 버전입니다.
Actor는 데이터 경합으로부터 상태를 보호하는 스레드 안전 참조 유형입니다. 컴파일러는 액터 속성에 대한 접근이 비동기 호출 또는 액터 격리 내에서만 가능함을 보장합니다. Actor는 수동 잠금 없이 공유 변경 가능 상태를 보호하기 위해 직렬 DispatchQueue를 대체합니다.
Structured Concurrency의 원칙은 다음과 같습니다. 모든 비동기 작업에는 부모가 있으며, 부모는 모든 자식이 완료될 때까지 완료되지 않습니다. 작업 A가 Task { await B() }를 시작하면 A는 B를 기다립니다. 이는 작업 누수를 방지하고 예측 가능한 수명 주기를 보장합니다.
Kotlin에서 async/await는 코루틴을 통해 구현됩니다. 코루틴은 언어의 경량 스레드입니다. 함수는 suspend 키워드(Swift의 async와 유사)로 표시됩니다. 시작하려면 launch(발사 후 망각) 또는 async(결과 포함)를 사용합니다. 결과를 얻으려면 await를 호출합니다.
Dispatcher는 코루틴이 실행될 스레드 풀을 결정합니다. UI에는 Dispatchers.Main, 네트워크/디스크에는 Dispatchers.IO, CPU 집약적 작업에는 Dispatchers.Default입니다. Structured Concurrency는 CoroutineScope를 통해 보장됩니다. 범위가 취소되면 모든 하위 코루틴이 취소됩니다.
JetBrains(2025)에 따르면, Google Play의 Android 앱 중 97%가 kotlinx.coroutines를 사용하며, 이 중 82%가 네트워크 요청에 async/await를 사용합니다. 코루틴은 Android에서 비동기 프로그래밍의 사실상 표준이 되었습니다. Kotlin Multiplatform도 코루틴을 지원하여 Android, iOS 및 서버 측에서 공유 비동기 코드를 사용할 수 있습니다.
각 언어에 대한 두 가지 예제를 살펴보겠습니다. 순차적 데이터 로드와 TaskGroup 또는 async/await를 사용한 병렬 요청입니다.
await는 사용자 데이터를 받을 때까지, 그 다음 친구 데이터를 받을 때까지 실행을 일시 중단합니다. 중첩된 클로저가 없습니다.
func loadProfile() async throws -> Profile {
let user = try await api.fetchUser()
let friends = try await api.fetchFriends(for: user.id)
return Profile(user: user, friends: friends)
}
TaskGroup은 작업을 병렬로 시작하고 결과를 배열에 수집합니다. 결과의 순서는 시작 순서와 일치하지 않을 수 있습니다.
func loadParallel() async throws -> [String] {
await withThrowingTaskGroup(of: String.self) { group in
group.addTask { try await api.fetchName() }
group.addTask { try await api.fetchEmail() }
group.addTask { try await api.fetchAvatar() }
var results = [String]()
for try await value in group { results.append(value) }
return results
}
}
suspend 함수는 다른 suspend 함수를 호출할 수 있습니다. Swift와 유사: 순차적 호출.
suspend fun loadProfile(): Profile {
val user = api.fetchUser()
val friends = api.fetchFriends(user.id)
return Profile(user, friends)
}
Kotlin에서는 각 요청에 async를 사용한 coroutineScope를 통해 병렬 처리가 달성됩니다.
suspend fun loadParallel(): List<String> = coroutineScope {
val name = async { api.fetchName() }
val email = async { api.fetchEmail() }
val avatar = async { api.fetchAvatar() }
listOf(name.await(), email.await(), avatar.await())
}
잊혀진 Task { } — 비동기 함수는 Task로 래핑하지 않고 동기 컨텍스트에서 호출할 수 없습니다. Swift에서는 오류와 함께 컴파일됩니다. Kotlin에서는 lifecycleScope 또는 viewModelScope를 통해 시작해야 합니다. 일반 함수에서 suspend 함수를 호출하려고 하면 컴파일되지 않습니다.
모든 함수를 비동기로 만들어야 하는 것은 아닙니다. I/O가 없는 CPU 집약적 작업은 async의 이점을 얻지 못합니다. .userInitiated QoS로 DispatchQueue에서 실행하는 것이 좋습니다. Async/Await는 I/O 바인딩 작업(네트워크, 디스크, 대기)에 최적화되어 있습니다.
메인 스레드에서 .result를 사용하면(비동기 함수 결과를 동기적으로 대기) 차단이 발생합니다. Swift에서 Task.synchronousWait는 Apple이 권장하지 않습니다. Kotlin에서 메인 스레드의 runBlocking은 안티패턴입니다. lifecycleScope를 사용하세요.
Task가 클래스 속성에 저장되지 않으면 컨텍스트를 벗어난 후 취소됩니다. Swift에서 Task는 범위가 지정되며 수명 주기는 생성 컨텍스트에 바인딩됩니다. 장기 실행 작업이 필요한 경우 Task를 속성에 저장하세요.
자주 묻는 질문
Async/Await는 큐를 만들 필요가 없는 언어 구조입니다. DispatchQueue는 스레드를 관리하기 위한 시스템 API입니다. Async/await는 언어 수준에서 상태 머신으로 컴파일되고, GCD는 OS 수준에서 시스템 콜로 컴파일됩니다.
아니요 — await는 함수를 일시 중단하지만 다른 작업을 위해 스레드를 해제합니다. 작업이 완료되면 함수는 풀에서 사용 가능한 모든 스레드에서 재개됩니다.
네 — Swift에서는 Task.cancel()을, Kotlin에서는 Job.cancel()을 사용합니다. 취소는 Structured Concurrency에 따라 모든 하위 작업에 전파됩니다. 코드는 Task.isCancelled 또는 ensureActive()를 통해 취소 플래그를 확인합니다.
MainActor는 속성과 메서드가 항상 메인 스레드에서 실행되는 액터입니다. 함수를 @MainActor로 표시하면 UI 업데이트가 올바른 스레드에서 발생함을 보장하여 DispatchQueue.main.async를 대체합니다.
Async/Await는 Swift(iOS 13+), Kotlin(kotlinx.coroutines 통해), Dart(Flutter), JavaScript/TypeScript, Python, C#, Rust 및 Go(문법적 설탕이 있는 goroutines 통해)에서 사용할 수 있습니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.