Explore structured concurrency in Swift

Swift 5.5는 Structured Concurrency라는 개념을 사용하여 동시성 프로그램을 작성하는 새로운 방법을 소개합니다. 구조적 동시성의 이면에 있는 아이디어는 매우 직관적인 구조적 프로그래밍을 기반으로 합니다.

https://i.imgur.com/L9C3aFg.png

초기의 컴퓨팅 프로그램은 제어 흐름이 사방으로 뛰어오를 수 있는 일련의 명령어로 작성되었기 때문에 읽기가 힘들었습니다.

https://i.imgur.com/KxgzHzM.png

오늘날에는 언어가 structured programming를 사용하여 더욱 균일하게 제어 흐름을 만들기 때문에 초기의 문제를 보기 어렵습니다.

예를 들어, if-then 문은 구조화된 제어 흐름을 사용합니다. 중첩된 코드 블록이 위에서 아래로 이동하는 동안에만 조건부로 실행되도록 지정합니다. Swift에서 이 블록은 정적 범위 지정도 존중하는데, 이는 이름이 둘러싸는 블록에 정의된 경우에만 표시된다는 것을 의미한다. 이것은 또한 블록에서 벗어날 때 블록에 정의된 변수의 수명이 끝난다는 것을 의미합니다. 따라서 정적 범위를 가진 구조화된 프로그래밍은 제어 흐름과 가변적인 수명을 이해하기 쉽게 만든다.

https://i.imgur.com/mNl6Dvz.png

좀 더 일반적으로, 구조화된 제어 흐름은 자연스럽게 순서를 지정하고 함께 중첩될 수 있습니다. 이렇게 하면 전체 프로그램을 위에서 아래로 읽을 수 있습니다. 이것이 구조화된 프로그래밍의 기초입니다.

오늘날의 프로그램은 비동기 및 동시 코드를 특징으로 하며 구조화된 프로그래밍을 사용하여 해당 코드를 더 쉽게 작성할 수 없었습니다.

구조화된 프로그래밍이 어떻게 비동기 코드를 더 단순하게 만드는지 살펴보겠습니다.

    // Asynchronous code with completion handlers is unstructured.
    func fetchThumnails(for ids: [String],
                        completion hanlder: @escaping ([String: UIImage]?, Error?) -> Void) {
        guard let id = ids.first else {
            return handler([:], nil)
        }

        let request = thumbnailURLRequest(for: id)
        URLSession.shared.dataTask(with: request) { data, response, error in
            guard let response = response,
                  let data = data,
            else {
                return handler(nil, error)
            }
            // ... check response ...
            UIImage(data: data)?.prepareThumnail(of: thumbSize) { image in
                guard let image = image else {
                    return handler(nil ThumbnailFailedError())
                }
                fetchThumbnails(for: Array(ids.dropFirst())) { thumbnails, error in
                    // ... add image to thumbnails ...
                }
            }
        }
    }

예를 들어, 인터넷에서 여러 이미지를 가져와서 축소판 크기로 순차적으로 크기를 조정해야 한다고 가정해 보겠습니다. 이 코드는 이미지를 식별하는 문자열 컬렉션을 가져와 비동기적으로 작업을 수행합니다. 이 함수는 호출될 때 값을 반환하지 않는다는 것을 알 수 있습니다. 이는 함수가 결과 또는 오류를 주어진 완료 핸들러에 전달하기 때문입니다.

두 번째 guard let을 사용하면 발신자가 나중에 응답을 받을 수 있습니다. 해당 패턴의 결과로 이 함수는 오류 처리를 위해 구조화된 제어 흐름을 사용할 수 없습니다. 그 이유는 하나가 아니라 함수 밖으로 던져진 오류를 처리하는 것이 이치에 맞기 때문입니다. 또한 이 패턴은 루프를 사용하여 각 썸네일을 처리하는 것을 방지합니다. 함수가 완료된 후 실행되는 코드는 핸들러 내에 중첩되어야 하므로 재귀가 필요합니다. 이제 이전 코드를 살펴보고 구조적 프로그래밍을 기반으로 하는 새로운 async/await 구문을 사용하도록 다시 작성해 보겠습니다.

    // Asynchronous code with async/await is structured.
    func fetchThumnails(for ids: [String]) async throws -> [String: UIImage] {
        var thumbnails: [String: UIImage] = [:]
        for id in ids {
            let request = thumbnailURLRequest(for: id)
            let (data, response) = try await URLSession.shared.data(for: request)
            try validateResponse(response)
            guard let image = await UIImage(data: data)?.byPreparingThumbnail(ofSize: thumbSize) else {
                throw
            }
            thumnails[id] = image
        }
        return thumbnails
    }

함수에서 완료 처리기 인수를 삭제했습니다. 반환 타입에 asyncthrows로 주석이 추가됩니다. 또한 아무것도 아닌 값을 반환합니다. 함수 본문에서 await를 사용하여 비동기 작업이 발생하고 해당 작업 후에 실행되는 코드에는 중첩이 필요하지 않다고 말합니다. 즉, 이제 thumbnail을 반복하여 순차적으로 처리할 수 있습니다. 또한 오류를 던지고 잡을 수 있으며 컴파일러는 내가 잊지 않았는지 확인합니다. async/await에 대한 자세한 내용은 Meet async/await in Swift 세션을 확인하세요.