Asynchronous programming
- 보편적으로 사용하지만 verbose, complex, incorrect 하다는 단점이 있음
- completionHandler 또는 delegate 콜백으로 값을 받음
Example
기존에 사용하던 방식으로 fetchThumbnail이라는 thumbnail 이미지를 받아오는 메서드를 작성해 보자.
- URLSession.shared.dataTask의 completion으로 data를 받는다.
- task는 async 하게 동작하므로 thread에서는 dataTask를 실행한 이후 다른 임무를 수행하다가 dataTask의 작업이 완료될 때 completion을 처리한다.
dataTask의 completion 내부에서 또다시 prepareThumbnail을 실행하고, completion으로 thumbnail을 받아 completion으로 던져준다.
여기서 이 코드의 문제점이 발견된다..! 그것은 바로 guard의 return 구문 앞에 completion을 적어주지 않았다는 것!
completion을 호출해주지 않으면 fetchThumbnail은 결괏값으로 뭐가 들어왔었는지도 모른 채 종결되고 만다. (당연히 thumbnail 업데이트도 없겠지?) Swift에서는 에러를 throw 하는 completion을 강제하지 않아서 빌드하고 실행해도 전혀 문제가 없다. 따라서 completion을 개발자가 적절히, 깜빡하지 않고 제때 잘 적어줘야 한다. 이 코드에서는 빼먹은 return 앞 completion들을 포함해 총 5개의 completion이 필요했지만, 코드가 길어질수록 셀프 체크해야 하는 부분은 늘어난다.
그렇다면, completion 핸들러에 Result Type(.success와 .failure)를 사용해 준다면 어떨까?
성공과 실패 그리고 error를 명확하게 전달할 수는 있지만 여전히 completion이 들어갔는지를 직접 체크해야 하는 것에는 변함이 없다.
Async and Await
- asynchronous cede를 쉽고 안전하게 작성할 수 있음
Example
위에 보았던 예제를 async와 await를 사용해 수정해 보자.
일단 대충 봐도 위에 completion을 여러 번 호출하는 것보다 간단해졌다. 클로저가 없고 모두 한 줄의 구문으로 끝이 나는 게 특징이다.
차이점을 한 번 살펴보면 아래 적어놓은 부분들을 발견할 수 있다.
- 기존에는 completion을 받고, return 타입이 없었는데, 여기는 completion이 없고 return 타입이 생김
- async throws가 추가됨
- URLSession.shared.data 메서드를 사용하고, 그 결괏값이 (data, response)라는 프로퍼티에 저장됨
- try await를 사용함
- thumbnail을 가져오는 maybeImage?.thumbnail에서도 await를 사용함
- 각 guard 구문에서 return 대신 throw를 사용해 에러를 던짐
그럼 이제 노란색으로 표시해 둔 부분을 자세히 들여다보자.
- 함수 선언부에 async throws는 fetchThumbnail 메서드가 async 하게 동작할 것이며 성공한 경우에는 UIImage를 작업에 문제가 생기는 경우 에러를 throw 하겠다는 의미이다.
- try await는 URLSession.data(for:delegate:) 메서드의 선언을 확인해보아야 한다. 아래 캡처한 이미지처럼 data 메서드는 에러를 throw 하기 때문에 try가 필요하다. 또한 async 하게 동작하기 때문에 async 메서드의 동작 완료를 기다린다는 표시인 await 키워드를 사용한다.
- thumbnail 앞에 await가 붙어있는 걸 확인할 수 있는데 요 부분은 메서드뿐만 아니라 property도 async 하게 동작할 수 있다는 것을 보여준다. (Initializer도..?!)
extension UIImage { var thumbnail: UIImage? { get async { let size = CGSize(width: 40, height: 40) return await self.byPreparingThumbnail(ofSize: size) } } }
read-only 프로퍼티에서만 async 사용이 가능하다
추가로 await는 for loop과도 함께 사용할 수 있다.
completion 없이 끝나는 async 코드라니..! 이렇게 좋을 수가 있나! 심지어 메서드에서는 UIImage를 리턴하거나 실패 시 에러도 throw로 관리하고 있기 때문에 에러 처리를 깜빡할 일도 없다.
Async와 Suspend?
A normal function call
function 내부의 코드 라인이 모두 실행될 때까지 thread를 사용한다. thread는 작업이 완료된 이후 다른 작업을 수행한다.
여기서는 fetchThumbnail 메서드가 thread를 점유하고 있다가 thumbnailURLRequest 메서드 호출이 됐을 때 thread는 thumbnailURLRequest 메서드를 수행한다. 여기서 return request가 되어야만 thread는 다시 fetchThumbnail 메서드의 작업을 실행한다.
An asynchronous function call
normal function과 동일하게 async 메서드를 호출하면 thread를 점유해 작업을 실행한다. 그러다 data(for:)과 같은 비동기 함수를 실행하게 되면 await, 즉 suspend 될 수 있다.
suspend가 된다는 것은 현재 점유하고 있던 thread의 제어권을 system에 다시 반환하는 것. 그렇게 되면 data 메서드는 잠시 suspend 되고, system이 판단하기에 더 중요한 작업을 우선 처리하다가 '아 이제 저거 빨리 다시 시작해야겠다!' 하면 thread를 data(for:) 메서드를 실행하도록 넘겨준다(resume). resume이 되어 다시 돌아왔을 때 fetchThumbnail의 기타 남은 작업을 모두 처리(throw 에러를 하거나 UIImage를 반환)하면 thread 사용이 끝나게 된다. (async와 await이라도 반드시 suspend 되는 건 아니라고 하는 걸 보면 system이 상황에 맞게 잘 조율해서 처리하는 듯 보인다.)
다시 fetchThumbnail 메서드를 보면서 await 되는 부분을 살펴보면
1. data(for:) 메서드가 thread에서 작업 중이다가
2. main thread에서 급하게 처리할 게 생기면
3. thread에서 작업되고 있던 data(for:) 메서드는 잠시 suspend 되고 (thread 제어권을 잃게 되고)
4. IBAction 작업이 모두 종료되면 다시 data(for:) 메서드가 실행된다. (resume)
async 하게 돌아가는 작업은 system에 thread를 넘겨주면서 suspend 될 수 있기 때문에 (== 작업이 다시 실행될 것을 기다릴 수 있기 때문에) await 키워드를 붙여 알린다.
여기서는 await 코드가 두 개 있기 때문에 한 번의 트랜잭션으로 작업이 끝나지 않을 수도 있다. (suspend 되었다가 다시 실행될 수도 있으므로 + thread 제어권을 잃었다가 system에서 다시 할당해 주기 때문에 같은 thread에서 작업하지 않을 수도 있겠군,..!)
Async/await facts
- async enables a function to suspend
- await marks where an async function may suspend execution
- Other work can happen during a suspension
- Once an awaited async call completes, execution resumes after the await
기존의 Closure를 async로 사용하기
withCheckedThrowingContinuation을 사용해 completion이 아닌 [Post]를 반환하도록 메서드를 수정할 수 있다. 단, 주의해야 할 점은 다시 실행 되어야 하는 부분에서 resume 코드를 꼭 추가해주어야 한다는것! 그렇지 않으면 [Post] 또는 에러를 반환할 수 없어진다. 또한 하나의 경우에 두 번의 resume을 호출 할 수 없고, if-else가 있다면 양쪽의 경우 모두 resume을 추가해주어야 정상 동작한다.
참고
https://github.com/apple/swift-evolution/blob/main/proposals/0296-async-await.md
https://developer.apple.com/documentation/foundation/urlsession/3767352-data
https://developer.apple.com/documentation/swift/checkedcontinuation
'WWDC' 카테고리의 다른 글
WWDC 2024 Swift Testing (0) | 2024.06.12 |
---|---|
WWDC 2024 Keynote (0) | 2024.06.11 |