Swift Concurrecy에서 비동기 개념의 확장?

 

비동기의 구체적인 개념에 대해서 Swift Concurrecy를 사용하면서 내가 생각했던 개념에 대해 다시 짚어봐야할 내용이 있는 것 같아 한번 알아보려고 한다.

 

이전에 Swift Concurrecy를 사용하기 전에 비동기 처리는 GCD의 DispatchQueue.main.async {} 이런 식으로 사용해서 이 일을 메인스레드에 맡겨 실행하고 나는 기다리지 않겠다 정도의 의미였다. 핵심은 작업을 큐에 넣고 끝이라는 것, 이 실행이 언제 끝나는지는 모르고, 끝났다는 알림을 받고 싶다면 completion handler 같은 콜백을 추가하여 붙여야 했다.

 

즉 정리하자면, 내가 일을 시키고 기다리지 않는다! 라는 개념으로 실행을 위임하는 단순한 개념이었다.

 

Swift Concurrecy에서 비동기는 단순히 일이 끝나는 것을 기다리지 않는다 정도가 아니라, 작업이 끝나는 시점을 표현식 단위로 다룰 수 있다! 라는 의미에 가깝다.

 

await를 쓰면 함수의 실행 흐름이 잠깐 멈췄다가 결과가 준비되면 다시 이어진다. → 이걸 바탕으로 구조적인 동기성 구현이 가능해진다는 점이며 아래와 같은 특징을 가지게 된다.

 

  • 일시 중단 가능 → 스레드를 점유하지 않고 다른 Task가 실행될 수 있다
  • 협력적이다 → Task, Actor, Structured Concurrency로 안전하게 관리 된다.
  • 결과 지향적 → async throws → ReturnType 같이 명확한 반환값과 에러 핸들링이 가능하다.
  • GCD(Grand Central Dispatch) : 작업 단위(클로저)를 큐에 던져 실행하고 완료 알림은 주로 콜백/그룹으로 직접 관리한다. 스레드 및 큐 개념이 전면에 드러나게 된다.
  • Swift Concurrecy(Async/Await, Task, Actor) : 함수/표현식 단위로 “언제 기다리고 언제 이어갈지”를 코드 흐름속에서 명시한다. 오류/취소/자원 공유를 언어 차원에서 구조적으로 다루게 됨

Swift Concurrency의 일시 중단(Suspension) vs 대기(Blocking)

  • await는 스레드를 멈추는게 아니라 함수 실행 흐름만 잠깐 멈춘다는게 핵심!
  • CPU 스레드는 다른 일을 처리할 수 있고, 필요한 값이 준비되면 흐름이 다시 이어진다
  • 반대로, sleep() 이나 semaphore.wait() 같은 블로킹은 스레드를 붙잡아버려 자원 낭비와 앱 프리징을 유발한다.
// ❌ 블로킹 방식
Thread.sleep(forTimeInterval: 3)
print("3초 후 실행") // 3초 동안 스레드가 멈춤

// ✅ 비동기 일시중단 방식
try await Task.sleep(nanoseconds: 3_000_000_000)
print("3초 후 실행") // 스레드는 멈추지 않고 다른 Task들이 실행됨 

블로킹은 예를 들면 은행 창구에서 서서 기다리는 것이고 일시 중단은 대기표 뽑고 카페 가서 커피 마시다가 호출 받는 다는 개념이라고 보면 된다!

 

협력적 비동기

Swift Concurrency의 모든 비동기 작업은 Task라는 단위로 실행되고, 이 Task들은 서로 협력해서 실행된다.

  • 부모 Task가 취소 되면 자식도 함께 취소되는 방식 (전파 구조)
  • Actor를 통해 데이터 접근을 직렬화 하여 레이스 컨디션 방지!
func fetchData() async throws -> String {
    try await Task.sleep(nanoseconds: 2_000_000_000)
    return "결과"
}

Task {
    do {
        let result = try await fetchData()
        print(result)
    } catch {
        print("작업이 취소됨")
    }
}

// 1초 후 취소
Task {
    try await Task.sleep(nanoseconds: 1_000_000_000)
    Task.cancelAll()
}

예를들어 부모 Task에서 오늘 여행 취소!! 라고 했을떈 자연스럽게 자식 Task도 취소되는 방식과 같은 형태라고 볼 수 있다!

 

결과 지향적 비동기

GCD는 결과를 콜백에서만 받을 수 있어 흐름이 끊긴다. Swift Concurrency는 async throws -> ReturnType 으로 결과, 에러, 흐름이 함수 시그니처에 드러난다.

  • GCD방식
API.fetchUser { result in
    switch result {
    case .success(let user): print(user)
    case .failure(let error): print(error)
    }
}
  • Swift Concurrency 방식
func loadUser() async throws -> User {
    try await API.fetchUser()
}

Task {
    do {
        let user = try await loadUser()
        print(user)
    } catch {
        print(error)
    }
}

GCD는 택배 기사님이 언제 올지 모르고, 오면 초인종이 울려야 온 지 알 수 있는 상태라면

Swift Concurrency는 앱에 자동 배송 알림 시스템이 있어서, 내가 코드 흐름 속에서 결과를 직접 받아 확인할 수 있다.

 

구조적 동기성

  • async let과 TaskGroup을 통해 여러 작업을 병렬로 실행하면서도, 결과를 모으는 부분은 동기 코드처럼 깔끔하게 쓸 수 있다.
  • GCD에서는 DispatchGroup과 콜백을 직접 관리해야 했던 부분이 Swift Concurrency에서는 훨씬 단순하고 가독성 있게 작성이 가능해 진다.
// Swift Concurrency
func loadDashboard() async throws -> Dashboard {
    async let user = API.fetchUser()
    async let posts = API.fetchPosts()
    async let notifications = API.fetchNotifications()

    return try await Dashboard(user: user, posts: posts, notifications: notifications)
}

코드를 통해 예시를 들어보면 GCD는 세 친구에게 어떤 부탁을 했는데 각각 언제 끝날지 몰라서 계속 전화해봐야하는 것이고, Swift Concurrency는 그룹 채팅방을 만들어서 결과를 동시에 받는다라고 생각할 수 있다.

 

Actor와 데이터 안전성

  • GCD에서는 Race Condition을 막으려면 직접 NSLock이나 직렬 큐를 사용해서 동시에 접근하는 것을 방지했다면,
  • Swift Concurrency는 actor 키워드로 하나의 테스크에 하나만 접근할 수 있도록 안정성을 언어 차원에서 보장하게 된다.
actor Counter {
    private var value = 0
    func increment() { value += 1 }
    func current() -> Int { value }
}

let counter = Counter()
await counter.increment()
print(await counter.current())