본문 바로가기

Swift 공부

WWDC 공부 정리 1차 Meet async/await in Swift

WWDC를 하나씩 정리해야할 필요성을 느끼게 되고 현재 재직중인 회사에서 네트워크 계층을 리팩토링하는 과정중

기존의 비동기코드의 completion Block을 async/await 새롭게 리팩토링하게되었고 async/await에 대한 지식이 필요하고 기존에는

async/await는 completion Block을 간단하게 쓸려고 async/await 아니야? 정도만 알고있던 지식의 견문을 넓히기 위해 

나 자신을 위해 블로그글을 작성합니다.

 

 

Swift async/await가 탄생한 배경 

completion Block / clouser의 코드는 너무 복잡하고 개발자들을 힘들게 하였고 이를 문제삼아 async/await가 탄생되었다.

그리고 completion Block / clouser 보다 더 안전하게 개발을 할 수 있게하기위해 async/await 탄생되었다

사전지식

동기식함수는 함수를 호출하면 함수가 호출된 스레드가 차단되어 해당 함수가 완료가 될때까지 기다리는 과정을 가지게됩니다.

비동기함수는 함수를 호출하게되면 해당 함수가 완료되지 않더라도 자유롭게 다른 함수들이 실행될수있고 작업중인 함수는 완료가되면 completion 을 통해 호출되어 알려주게됩니다.

-> 중요한차이 동기식함수는 해당함수가 완료가되는동안 스레드에 다른 리소스가 접근하지 못하도록 차단하지만 비동기식 함수는 해당 스레드의 자원을 사용할수있다는 점에 있다. (동기 / 비동기) 

 

- 영상에나오는 fetchThumbnail 코드 아래코드에서 문제를 찾아보자

func fetchThumbnail(for id: String, completion: @escaping (UIImage?, Error?) -> Void) {
    let request = thumbnailURLRequest(for: id)
    let task = URLSession.shared.dataTask(with: request) { data, response, error in
        if let error = error {
        	//에러가 있을경우 completion 을 통해 전달
            completion(nil, error)
        } else if (response as? HTTPURLResponse)?.statusCode != 200 {
        	//에러가 있을경우 completion 을 통해 전달
            completion(nil, FetchError.badID)
        } else {
            guard let image = UIImage(data: data!) else {
            	//에러가 있을경우 completion 을 통해 전달
                completion(nil, FetchError.badImage)
                return
            }
            image.prepareThumbnail(of: CGSize(width: 40, height: 40)) { thumbnail in
                guard let thumbnail = thumbnail else {
                	return
                }
                //성공이였을경우에 썸네일 이미지를 전달
                completion(thumbnail, nil)
            }
        }
    }
    task.resume()
}
            image.prepareThumbnail(of: CGSize(width: 40, height: 40)) { thumbnail in
                guard let thumbnail = thumbnail else {
                // 이곳에 집중을해보자 기존의 컴플리션 블럭은 optinal값을 제거하기위해
                // guard let, if let 등을 사용하게되는데 이때 return만으로 처리해여 
                // 에러에 대한 완료값을 받지못하는 케이스가 생길수있다
                // 이런 부분들은 실제로 개발하면서 많이 발생하는 케이스이고 
                // 개발자가 테스트하는 과정에서 쉽게 발견되지 않는 케이스인 경우가 많다.
                	return
                }
                //성공이였을경우에 썸네일 이미지를 전달
                completion(thumbnail, nil)
            }
        }

 

 

위와같은 이슈는 async await을 통해 완벽하게 해소가 될수있다.

 

//thumbnailURLRequest를 만들고
//URLSession을 통해 데이터를 가져오고
//해당 데이터의 에러처리를 진행하고
//가져온 이미지 데이터를 통해 이미지를 만들고
//해당 이미지데이터가 문제가없는 이미지인지
//이모든과정이 순차적으로 이루어지는 비동기 코드인것을 쉽게 읽을수있다. 

func fetchThumbnail(for id: String) async throws -> UIImage {
    let request = thumbnailURLRequest(for: id)  
    let (data, response) = try await URLSession.shared.data(for: request)
    guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw FetchError.badID }
    let maybeImage = UIImage(data: data)
    guard let thumbnail = await maybeImage?.thumbnail else { throw FetchError.badImage }
    return thumbnail
}

 

같은 코드지만 async throws 를 통해 컴파일러 단계에서 처리되지않은 예외처리에를 xcode가 잡아줄수있게한다.

 

이렇게 async await를 사용하게되면 약 20줄의 코드였던 fetchThumbnail 코드가 단 6줄의 예외처리가 누락되지않은 코드로 작성될수있고 기존의 복잡한 컴플리션블럭이 아닌 직선의 코드이기때문에 개발자의 가독성에 큰 도움을 준다.

 

 

Swift 5.5 부터 지원해주는 기능

 

get  을 사용할때 throw를 처리할수있게 도와준다. 이러한 부분은 async을 사용할때 크게 유용하게 작용할수있을것같다.

 

추가적으로 for문을 사용할때도 async 키워드를 사용할수있는데 이부분이 궁금하신분들은 영상을통해 확인해주시면 좋을것같다.

extension UIImage {
    var thumbnail: UIImage? {
        get async {
            let size = CGSize(width: 40, height: 40)
            return await self.byPreparingThumbnail(ofSize: size)
        }
    }
}

 

 

영상을 보면서 가장 중요했던 부분이다

 

영상에서 발표자는 키워드는 비동기 기능이 일시중단될 수 있다고 이야기해준다.

비동기 기능이 일시중단될 수 있다? 이게 무슨의미인지 솔직히 자세하게는 몰랐다.

 

그래서 영상에서 자세히 설명해주는데 이부분이 굉장히 흥미로웠다.

 

함수를 호출하면 해당 함수가 함수가 실행중인 스레드에 제어권을 가지게 된다.

 

일반 함수인 경우 스레드는 완료될때까지 해당 함수를 대신하여 작업을 수행하는데 완전히 사용됩니다 (?)

해당 작업은 함수 자체의 본문에 있을수도 있고 호출하는 다른 함수에 있을수도있다.

결국 해당 함수는 값을 반환하거나 오류를 발생시켜 종료된다.

이 부분을 자세히 설명해보자면

 

  • 함수 호출과 제어권:
    • 함수를 호출하면, 프로그램의 실행 흐름(제어권)이 그 함수로 넘어갑니다.
    • 즉, 프로그램은 그 함수의 코드를 실행하기 시작합니다.
  • 일반 함수와 스레드:
    • 일반 함수가 실행될 때, 그 함수를 실행하고 있는 스레드는 해당 함수의 작업만을 수행합니다.
    • 스레드는 그 함수가 완료될 때까지 다른 작업을 하지 않고 기다립니다.
  • 함수의 작업 범위:
    • 함수가 수행하는 작업은 그 함수 자체의 코드일 수도 있고,
    • 그 함수 안에서 호출하는 다른 함수들의 코드일 수도 있습니다.
  • 함수의 종료:
    • 함수는 보통 두 가지 방법으로 끝납니다: a) 값을 반환하며 정상적으로 종료 b) 오류(에러)를 발생시키며 비정상적으로 종료

이게 일반적인 동기함수들의 동작 방식이다.

 

하지만 비동기일 경우 완전 다른방식을통해 스레드의 제어를 포기하게 된다.

 

영상에서는 일시 중단을 통해 스레드 제어를 포기할수있다고한다 자세히 살펴보자.

 

비동기함수도 일반함수와 마찬가지로 비동기 함수를 호출하면 해당 함수에 스레드 제어 권한을 부여하게된다.

 

일단 실행되면 비동기 함수가 일시 중단(await) 이 될수도있다. 이렇게 일시 중단되면 스레드의 제어를 포기한다는 의미이다.

 

함수가 일시 중단이 되게 된다면 시스템은 자유롭게 스레드를 이용하여 다른 작업을 진행할수있다(동시성)

 

그리고 어느 시점에서 시스템은 수행해야할 가장 중요한 작업이 이전에 일시 중단된 비동기 기능을 실행하는 것

 

즉 일시 중단되었던 함수가 다시 동작을 언제해야하는지 시스템에서 신경써야한다. 다시 resume하게 된다면 비동기 함수가

 

다시 스레드를 제어하고 해당 작업을 계속 진행할수있게한다.

 

함수가 일시 중지된 동안 다른 작업을 수행할 수 있다는 사실이 Swift가 비동기 호출을 wait 키워드로 표시하도록 주장하는 이유다.

 

 

그림에서처럼 두개의 await의 사이에 무슨일이 생길지는 아무도 모른다. 해당 예시함수에서는 전역변수를 사용하지않았지만

만약에 저 사이에 전역변수를 사용하는 코드가있었다면 중간의 과정에 전역변수가 변경되어 개발자가 의도하지않은 전혀 다른 방향으로 코드가 흘러갈수있다는 점을 인지해야하고 위에서 시작했던 스레드와 아래에서 시작되는 스레드는 재개될때 달라질수도있다는점이다.

 

이런것들을 해결하기위해 Actor를 한번 알아보라고하는데 이 부분은 다음 글로 정리를 해볼려고한다.

 

async/await의 중요하게 기억해야할것

 

함수를 비동기로 표시하면 해당 함수가 일시 중지되도록 허용이 된다. (즉 스레드의 제어권을 포기할수있게되고 작업하던 스레드에 다른작업이 들어올수있도록 시스템에게 넘겨줄수있다.) 그리고 함수가 자신을 일시 중지하면 호출자도 일시 중지가된다. 그렇기에 호출자도 비동기로 되어야한다.

 

비동기 함수에서 여러번 일시 중단될수있는 위치를 체크하기위해 wait라는 키워드가 사용된다. 

 

비동기 기능이 일시 중단되는동안 스레드는 차단이 되는게아니다.

 

비동기 함수가 다시 시작되면 호출한 비동기 함수에서 반환된 결과가 원래 함수로 다시 유입되고 중단된 부분부터 실행이 계속된다.

 

 

// Existing function
func getPersistentPosts(completion: @escaping ([Post], Error?) -> Void) {       
    do {
        let req = Post.fetchRequest()
        req.sortDescriptors = [NSSortDescriptor(key: "date", ascending: true)]
        let asyncRequest = NSAsynchronousFetchRequest<Post>(fetchRequest: req) { result in
            completion(result.finalResult ?? [], nil)
        }
        try self.managedObjectContext.execute(asyncRequest)
    } catch {
        completion([], error)
    }
}

//withCheckedThrowingContinuation 키워드를 통해 
//completion을 비동기로 변경할수있다 resume 키워드를 통해 값을 방출하게되는데
//resume이 만약에 방출되지 않는다면 swift는 경고를 방출합니다.
// Async alternative
func persistentPosts() async throws -> [Post] {       
    typealias PostContinuation = CheckedContinuation<[Post], Error>
    return try await withCheckedThrowingContinuation { (continuation: PostContinuation) in
        self.getPersistentPosts { posts, error in
            if let error = error { 
                continuation.resume(throwing: error) 
            } else {
            //만약에 이부분이 없다면 경고를 표시해준다.
                continuation.resume(returning: posts)
            }
        }
    }
}