이 포스트는 다음 버전을 기준으로 작성되었습니다.

  • iOS 15.6
  • Swift 5.6.1
  • Xcode 13.4.1

Intro

이번 글에서는 async let 에 대해 공부해 보겠습니다. 이전 포스트 Concurrency - Async, Await, 그리고 Task 알아보기Concurrency - Continuation 활용하기 에서 학습한 내용들을 어느정도 이해하셨다면, async let 은 상대적으로 간단하게 느껴질 거에요. 만약 아직 async/await 이 익숙하지 않다면 이전 포스트를 먼저 읽어주세요.

우선 async let 은 언제 사용하면 되는지를 알아봅시다. 그동안 우리가 사용한 비동기 함수에는 공통적인 부분이 있었어요. 바로 한번에 하나의 비동기 작업만 처리 했었다는 것이죠. 그렇다면 여러 개의 비동기 작업을 처리해야 하는 상황에서도 그냥 기존 방식대로 하면 되는걸까요?

테스트를 해보면 기존 방식으로도 여러 개의 비동기 작업이 정상적으로 처리되는 것을 알 수 있습니다. 하지만 좀 더 테스트를 해본다면 비효율적인 부분이 있는 것도 발견할 수 있습니다. async let 은 이런 비효율적인 부분을 효율적으로 처리할 수 있게 도와줍니다. 그럼 기존 방식과 어떤 차이가 있는지 바로 알아볼까요~


Usage Examples

Concurrency - Async, Await, 그리고 Task 알아보기 에서 사용한 예시를 그대로 가져와서 실습을 진행해 보겠습니다.

let url = URL(string: "https://api.quotable.io/random")

enum NetworkError: Error {
    case badUrl
    case badResponse
    case communicationError
    case decodeFailed
    case noData
}

struct Quote: Decodable {
    let content: String
}

func getQuote(from: URL?) async throws -> Quote {
    guard let url = url else {
        throw NetworkError.badUrl
    }
    let (data, response) = try await URLSession.shared.data(from: url)
    if let response = response as? HTTPURLResponse,
       !(200..<300).contains(response.statusCode) {
        throw NetworkError.badResponse
    }
    guard let quote = try? JSONDecoder().decode(Quote.self, from: data) else {
        throw NetworkError.decodeFailed
    }
    return quote
}

여러 개의 비동기 작업 진행을 위해 Quote 를 여러 번 요청하여 받은 데이터를 배열에 담아 출력하는 함수를 작성해 볼게요. 우리가 그동안 학습한 내용을 바탕으로 작성하면 다음과 같은 코드가 됩니다.

func getQuotes() {
    Task {
        let firstQuote = try await getQuote(from: url)
        let secondQuote = try await getQuote(from: url)
        let thirdQuote = try await getQuote(from: url)
        let quotes = [firstQuote.content, secondQuote.content, thirdQuote.content]
        print(quotes)
    }
}

정상적으로 잘 작동하네요 ㅎㅎ 하지만 사실 이 코드에는 문제가 하나 있습니다. 각 다운로드는 먼저 시작된 다운로드가 완료되기 전까지 시작되지 않고 대기하는 방식이라는 점 이에요. 지금은 작업이 3개뿐이므로 금새 끝났지만 만약 작업이 3,000 개 였다면 얼마나 오랜 시간이 걸릴지 끔찍합니다.

async let 적용하기

여러 개의 비동기 작업을 이전 비동기 작업의 종료 여부와 관계없이 처리하고자 할 때 바로 async let 을 사용하면 됩니다. async let 의 사용법은 간단합니다. 변수 선언 시 async let 으로 선언하고 데이터를 활용하는 단계에서 await 키워드로 기다리게 하면 끝이에요. 코드를 직접 보면서 배워볼게요.

아! 이번 코드는 꼭 Xcode Project 에서 실행해야 합니다. 현재 포스트 작성 시점에서는 Playground 에서 에러가 발생하며 동작하지 않습니다.

func getQuotes() {
    Task {
        async let firstQuote = getQuote(from: url)
        async let secondQuote = getQuote(from: url)
        async let thirdQuote = getQuote(from: url)
        let quotes = try await [firstQuote.content, secondQuote.content, thirdQuote.content]
        print(quotes)
    }
}

조금 전 얘기했던대로 비동기 함수의 리턴값을 async let 로 선언한 변수에 할당하고 해당 값을 배열에 할당할 때 await 키워드로 비동기 작업을 기다리게 해주었습니다.

이전 코드와 결과물에는 차이가 없지만 작동방식은 완전히 달라졌습니다. 이렇게 async let 으로 선언을 하면 더 이상 먼저 시작된 비동기 작업이 끝날 때까지 기다리지 않고 다음 작업을 시작합니다. 사용법이 정말 간단하지 않나요?

단 사용 시에 몇가지 반드시 알아두어야 할 것 들이 있습니다. asyncs let 은 이전 작업의 완료 시점과 관계없이 바로 다음 요청을 보내므로 끝나는 순서가 보장되지 않습니다. 그리고 함수 내부 즉 local variable 형태로만 사용할 수 있습니다. 즉 전역변수나 클래스 변수로는 사용할 수 없다는 것 기억해 두세요!


Wrap Up

이렇게 async let 에 대해 알아보았습니다. async let 은 비동기 작업 시 다른 작업이 끝나는 것을 기다리지 않고 시작할 수 있게 할 뿐만 아니라, 모든 비동기 작업이 끝난 후 다음 필요한 작업을 처리할 수 있게 보장해 주기도 한다는 것 알아두시면 필요할 때 유용하게 사용할 수 있을거에요.


Entire Code

import UIKit

let url = URL(string: "https://api.quotable.io/random")

enum NetworkError: Error {
    case badUrl
    case badResponse
    case communicationError
    case decodeFailed
    case noData
}

struct Quote: Decodable {
    let content: String
}

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        getQuotes()
    }
    
    func getQuote(from: URL?) async throws -> Quote {
        guard let url = url else {
            throw NetworkError.badUrl
        }
        let (data, response) = try await URLSession.shared.data(from: url)
        if let response = response as? HTTPURLResponse,
           !(200..<300).contains(response.statusCode) {
            throw NetworkError.badResponse
        }
        guard let quote = try? JSONDecoder().decode(Quote.self, from: data) else {
            throw NetworkError.decodeFailed
        }
        return quote
    }

    func getQuotes() {
        Task {
            async let firstQuote = getQuote(from: url)
            async let secondQuote = getQuote(from: url)
            async let thirdQuote = getQuote(from: url)
            let quotes = try await [firstQuote.content, secondQuote.content, thirdQuote.content]
            print(quotes)
        }
    }
}

References

Async let explained: call async functions in parallel