Swift Concurrency

Continuations

Swift Concurrency를 적용하게 되면 연관된 모든 작업들이 전부 async 함수로 선언되어야 사용할 수 있게 된다.
보통 비동기 처리를 하게 되면 RxSwift나 Combine과 같은 반응형 프레임워크를 사용하며 값 방출을 기다리거나 콜백함수를 주입해 작업순서를 조정하는데, 이런 기존 코드들이 의존성이 걸려 있거나 라이브러리 속에서 구현되어 직접 async function으로 마이그레이션하기가 난감할 때가 있다.
이럴 때 Continuation을 사용하면 비동기 처리를 안전하게 변환할 수 있다.

Continuation 함수들은 는 자유함수로 구현되어 있고, 당연히 해당 함수가 async로 선언되어 적어도 Cuncurrency context 안에서 실행되어야 한다.

에러를 던지는 withCheckedThrowingContinuation, 그렇지 않은 withCheckedContinuation이 있어 적당히 상황에 맞게 고르면 되는데, 보통 Result로 하겠지만 요즘 들어 Swift Concurrency에서는 그냥 에러를 던져서 전통적인 처리를 하는게 낫지않나 하는 생각이 들고있긴 한다.

/// 콜백을 받는 함수
func work(completionHandler: () -> ())

/// Continuation 래핑
func workContinuation() async {
    await withCheckedContinuation { continuation in
        work(completionHandler: {
            continuation.resume()
        })
    }
}

/// 콜백 사용
work(completionHandler: {
    print("After work")
})

/// Contination 사용
await workContinuation()
print("After work")

Continuation을 열었으면 무조건 resume함수를 콜해야 한다. 그렇지 않으면 Task가 잠기게 되고 계속 기다리게 된다. 결과가 성공이던 실패던 상관없이 resume()을 호출해야 하는데, 에러일 경우 throwing할 수 있다. 기본적으로 반환형을 지정하면 resume에서 돌려줄수 있는데, Void와 Result는 오버로딩 되어 있어 바로 쓸 수 있다.
당연히 resume을 2번 이상 콜해도 안되는데, 개발자가 주의해서 사용해야 한다. 이 부분은 웹뷰 리다이렉션 콜백이랑 비슷한 느낌을 받았다.
기존의 FCM 토큰 받아오거나 콜백으로 사용하는 부분을 몇번 래핑해서 사용 중이다.

Actor

Swift에도 Actor가 드디어 생겼다. WWDC에서 엄청 설명을 길게 해놨는데, 사실 기본적인 상호배제를 언어단에서 구현해 준 것이다.
Actor 내부를 직접 컨트롤할 수가 없고, mutating함수를 통해야 하는데, 상호배제를 기다려야 하기 때문에 Actor 외부에서 전부 await로 변하게 된다. getter도 마찬가지.

이미지 URL 배열로부터 네트워킹하여 받아와서 Data로 만드는 동작이 있었는데, 기존에는 딕셔너리로 되어 있어 Tread-safe하지 않기 때문에 NSLock으로 컨트롤 해야했다.
Actor도 공부해볼 겸 다시 작성했다.

struct AssetModel {
    var name: String?
    var data: Data?
}

actor AssetModelHolder {
    private var entities = Set<AssetModel>()
    
    func add(assetModel: AssetModel) {
        self.entities.insert(assetModel)
    }
    
    var entityList: [AssetModel] {
        return Array(self.entities)
    }
}

withThrowingTaskGroup과 같이 사용하면 여러 개의 작업을 동시에 실행하면서 상호 배제와 작업 완료 시점을 보장받을 수 있다.


let assetModelHolder = AssetModelHolder()

await withThrowingTaskGroup(of: Void.self) { group in
    /// 네트워킹
    let name = ""
    let data = Data()

    await assetModelHolder.add(.init(
        name: name,
        data: data
    ))
}

await assetModelHolder.entityList

Async callback

Swift Concurrency에서 async 클로저도 주입할 수 있다. 작업 흐름이 UI쪽으로 넘어가거나 사용자 입력을 기다리는 등 한번에 이루어지지 않게 되면 Continuation으로 붙이기가 애매해지는데, 이 때 async 클로저를 잘 주입하게 되면 원하는 값을 원하는 타이밍에 얻어낼 수 있다.

final class Worker {
    struct Callback {
        var onWork: (() async -> ())?
    }
    
    func work() async {
        print("DO WORK")
        await self.callback.onWork?()
    }
    
    var callback = Callback()
}

let worker = Worker()
worker.callback = .init(
    onWork: {
        print("AFTER WORK")
    }
)

Task {
    await worker.work()
    // "DO WORK"
    // "AFTER WORK"
}

보통 클라이언트에서 비동기 처리할 일이 네트워킹 이외에 많지는 않지만 구현하기 나름이라 Swift Concurrency를 잘 사용하면 가독성 있게 짤 수 있다.
그동안 Combine에서 비동기 처리에 Future를 사용하거나 했는데 앞으로는 비동기 처리는 Swift Concurrency로 구현하고 Combine은 상태관리나 바인딩 할때에나 많이 사용할 것 같다.