본문 바로가기

Swift 5.5의 async/await 살펴보기

기술적인 이야기/애플 플랫폼 개발 2021. 7. 13.
반응형

Swift는 전통적(?)으로 GCD(Grand Central Dispatch)라 불리는 병렬 프로그래밍 패러다임을 제공해왔다. 하지만 이 방식은 GCD(Grand Central Dispatch)의 Dispatch Queue를 활용한 모델로 현재로 치자면 코딩하기가 귀찮은 좀 구식인 스타일이었다. 문법만 보면 사실상 Objective-C를 위한 기능이라고 생각될 정도였으니 말이다.

어쨌든 이를 개선하기 위한 여러 요구가 있어왔다. 그리고 드디어 Swift 5.5부터 새로운 비동기 프로그래밍 패턴으로 동시성(Concurrency) 개념이 지원되면서 (이제야) asyncawait 키워드가 도입되었다. 내부에서는 여전히 GCD를 사용할 수는 있겠으나 표면적으로 이제는 동시성을 이용한다고 생각하면 될 것 같다.

이미 유명한 Javascript Promise 등을 통해 asyncawait 키워드는 잘 알려져 있는데 이번에 공개된 Swift의 그것도 사실상 비슷한 느낌인 것 같다.

참고로 이 글에서 사용된 예제들은 Xcode 13 Beta 2를 기준으로 작성되었기 때문에 나중에 정식 버전이 나오거나 혹은 Swift 버전이 올라감에 따라 바뀔 여지가 충분히 존재한다.

async

async 키워드는 비동기 태스크를 정의한다. 다르게 표현해서 async 마크가 표기된 함수는 비동기로 동작하는 함수가 된다.

func someWork() async throws -> String {
    ...
}

위 예제처럼 async 마크 위치는 함수 매개변수 선언 뒤이고 throws의 앞이다. 여기서 throws는 반드시 필요한 것이 아니라 그저 위치 설명을 위해서 추가한 것이니 오해는 하지 말자.

await

await는 비동기 함수의 실행을 동기시키는 명령이다. 즉 현재 스레드에서 해당 비동기 함수의 실행이 끝날 때까지 대기한다는 말이다.

func doWhatWithSomeWork() async {
    ...
    try? await someWork()
    ...
}

await는 블로킹(blocking)을 유발하므로 async로 마크된 비동기 함수 내에서만 쓸 수 있다. 당연하게도 await중인 함수의 실행이 완료될 때까지 이 함수의 실행은 중간에 멈추게(blocking) 된다.

await를 사용할 수 있는 곳은 여러 곳이 있는데 위치가 미묘하긴 하다. 예를 들어 for 루프의 경우 공식 홈페이지 소개 내용에 아래와 같은 식의 예제를 소개하고 있다.

import Foundation

let handle = FileHandle.standardInput
for try await line in handle.bytes.lines {
    print(line)
}

아마도 handle.bytes.lines는 비동기 Getter인 모양이다.

반응형

동기 코드 안에서의 비동기

사실 async는 함수가 아니라 블럭 자체에도 마크할 수도 있다.

func someSyncFunc() {
    ...
    async {
        try? await someWork()
    }
    ...
}

읽기는 쉬운 편이라 따로 설명은 필요 없을 것 같다.

단, 엄밀하게 말해서 이 블록은 사실 클로저(Closure)다. 즉 이 문법은 비동기 클로저(async closure)를 구현하는 문법이다.

업데이트: 그런데 위 방법이 이제는 Deprecated 되었다. 이제는 그냥 Task를 쓰자.

Task {
    try? await someWork()
}

위에서 쓰인 Task 함수의 클로저는 이미 async로 마크되어 있어서 내부에서 await를 써도 별 문제가 없다.

병렬 실행

한 번에 여러 작업을 동시에 실행하고자 할 때는 아래와 같은 async-let 문법의 코드를 작성할 수 있다.

async let james = someWork(name: "James")
async let conrad = someWork(name: "Conrad")
async let michael = someWork(name: "Michael")

let results = await [james, conrad, michael]

마치 Await들을 이용해 배열을 만든 느낌의 코드다. 가독성은 꽤 괜찮은 것 같다.

이 외에도 Task Groups를 활용하는 방법도 있다. 아래 예제는 공식 홈페이지의 예제이다.

await withTaskGroup(of: Data.self) { taskGroup in
    let photoNames = await listPhotos(inGallery: "Summer Vacation")
    for name in photoNames {
        taskGroup.async { await downloadPhoto(named: name) }
    }
}

태스크 그룹은 동적으로 병렬 태스크를 구성해야 할 때 유용한 방식인 것 같다. 위의 방식 외에도 부모-자식 태스크를 구성하는 등 다양한 용도로 사용되니 일단 이름은 알아두면 좋을 것 같다.

Actors

Swift 5.5 부터 액터(actor)라 불리는 새로운 타입이 추가되었다. Class와 비슷하게 특수 구조체를 설계할 수 있는 레퍼런스 타입이면서 스레드 안정성을 보장하기 위해 동시에 하나의 Task에서만 접근을 보장한다. 아래는 공식 홈페이지의 예제를 뭉뚱그린 코드다.

actor TemperatureLogger {
    let label: String
    var measurements: [Int]
    private(set) var max: Int

    init(label: String, measurement: Int) {
        self.label = label
        self.measurements = [measurement]
        self.max = measurement
    }

    func update(with measurement: Int) {
        measurements.append(measurement)
        if measurement > max {
            max = measurement
        }
    }
}

클래스를 대체한다고는 적었지만 사실 클래스와 완전히 동일한 것은 또 아니다. 예를 들어 var로 선언된 가변형 프로퍼티는 외부에서 수정 시에는 외부와 격리되는 규제를 받는다. 그리고 액터의 프로퍼티와 메서드를 외부에서 참조하려면 반드시 await로 접근해야 한다.

let logger = TemperatureLogger(label: "Temp", measurement: 0)

// 참조할 수 있는 프로퍼티나 메서드는 await로만 접근 가능
let max = await logger.max
await logger.update(with: 2)

// 가변형 프로퍼티 수정은 외부와 격리되기 때문에 아래 코드는 오류 발생:
// Actor-isolated property 'measurements' can only be mutated from inside the actor
await logger.measurements.append(1)

당연하게도 액터 내부에서는 딱히 자신의 프로퍼티나 메서드를 참조하는 데 await가 필요없다. 어찌 보면 당연한 디자인 같다.

결론적으로 비동기 코드 안에서 자주 참조될 공유 객체 모델이라면 이런 액터로 구현하면 딱 맞는 형태일 것 같다. 클래스는 애초에 레퍼런스 타입이라 동시에 여러 스레드에서 하나의 메모리에 접근해서 동시에 수정을 가하는 문제가 많았기 때문에 이런 특수 타입으로 공유 객체의 접근을 보장하려는 목적으로 등장한 것 같다.

그렇다면 구조체(struct)는 왜 이런 게 없는가 생각된다면 일단 Swift의 값(Value) 타입이란 게 뭔지부터 다시 생각해보는 것이 좋을 것 같다. 값 타입은 인스턴스 공유가 애초에 불가능하게 설계되어 있다.

728x90

Xcode Beta의 Playground에서 문제점

플레이그라운드에서 코드를 시험해 볼 때 문제를 좀 겪었다. 아래와 같은 이슈다.

error: cannot find 'async' in scope

이 외에도 await라던가 혹은 새로 추가된 Task도 찾을 수 없다는 오류가 났었다.

매뉴얼에 명시된 대로라면 이들은 모두 표준 라이브러리에 포함되기 때문에 별도의 패키지를 임포트 할 필요는 없는 게 맞다. 하지만 현재 베타에는 아래와 같이 동시성 모듈을 임포트 해야 한다.

import _Concurrency

이후 위와 같은 오류는 더 이상 발생하지 않았다.

이 외에도 플레이그라운드에서 여러 비동기 코드를 테스트하는 데 어려움이 많았다. 그냥 일반 프로젝트로 시험해 보는 게 스트레스를 받지 않는 것 같아서 더 좋을 것 같다.

마무리

사실 asyncawait 자체는 흔한 형태(?)라서 굳이 설명할 필요는 없을 정도가 되었을지도 모른다. 그만큼 Swift에는 늦게 등장한 개념이니 말이다. 어쨌건 간에 이제 최신 버전의 플랫폼을 지원한다면 비동기 프로그래밍이 상당히 조금은 편해질 것은 당연해 보인다.

이 외에도 살펴봐야 할 것이 너무나 많다. 기회가 되면 별도로 정리할 계획이다.

관련된 글들

 

Concurrency — The Swift Programming Language (Swift 5.5)

Concurrency Swift has built-in support for writing asynchronous and parallel code in a structured way. Asynchronous code can be suspended and resumed later, although only one piece of the program executes at a time. Suspending and resuming code in your pro

docs.swift.org

 

Swift 5.5의 Continuation

Swift 5.5의 동시성(Concurrency) 기능의 강화로 드디어 async 와 await 를 사용할 수 있게 되었다. 하지만 우리가 사용하게 될 기존의 프레임워크들은 여전히 이런 연속성 구현이 빠진 형태도 많을 것이

seorenn.tistory.com

 

Swift async/await

Swift의 새로운 비동기 프로그래밍 모델

seorenn.github.io

728x90
반응형

댓글