포스팅 하기 앞서, 먼저 알아야 할 단어들을 정리하자면
즉, 비동기 != 동시성 이다.
과거에는 함수의 파라미터에 @escaping 키워드를 사용한 함수 타입의 Closure 를 통해 비동기 처리를 하였다.
그러면서 코드가 장황하고(verbose)
, 복잡하고(complex)
, 부정확(incorrect)
해졌고, 오류처리가 어려웠다.
Swift 5.0부터 새롭게 도입된 enum 타입의 Result를 통해 Success/Error를 처리하는 코드가 나아졌다고는 하지만, 여전히 코드는 못생겼고 completion 미호출에 대한 리스크는 여전했다.
뿐만 아니라, 클로저가 중첩될수록 (Deeply-nested closures) call-back 지옥에 빠지기도 쉬웠다. (이를 파멸의 피라미드 (Pyramid of Doom)
라고 한다.)
completion handler를 통한 비동기 처리의 단점을 해결하기 위해
WWDC21 Swift 5.5부터 async/await 키워드가 새롭게 등장했고, 이로 인해 비동기 코드를 마치 동기식 코드처럼 사용할 수 있게 되었다.
async/await를 적용하면 코드가 훨씬 간단해지고, straight-line code
를 작성할 수 있게 된다.
함수명 뒤에 async 키워드가 붙으면 비동기 함수이다.
async
함수는 동시 컨텍스트 (Concurrent Context) 에서만 실행이 가능하다.
async
함수를 호출하기 위해서는 await 키워드가 필요하다.
따라서, data(for:delegate:) 는
async
함수임을 알 수 있다.
WWDC21 - Meet async/await in Swift의 15:03 즈음에
"The keyword (await) indicates that your async function might suspend there." 라고 설명한다.
즉, await
키워드는 async 함수를 '일시 중지' 한다.
(그러나 await
키워드가 있다고 해당 함수가 무조건 suspend 되는것은 아니다.)
Suspend 된다는 것은 'thread가 다른 일을 할 수 있도록 제어권을 포기한다.' 라는 뜻이다.
thread에 대한 제어권을 포기하는 방법에는 2가지가 있다.
동기 (sync) 함수의 경우
fetchThumbnail (이하 caller)에서 thumbnailURLRequest (이하 callee)를 호출하게 되면 caller가 실행되던 스레드의 제어권 (Control of the thread) 을 callee에게 전달한다.
이제 callee가 끝날 때까지 스레드는 점유되어 (occupied) 다른 일을 하지 못하게 된다.
callee가 끝나면 다시 스레드의 제어권을 caller에게 되돌려준다.
즉, 동기식일 경우 caller가 callee를 호출하면 thread-blocked 되어 callee가 완료될 때까지 기다린다.
🗣️ "That's the only way that a normal function can give up control of a thread: by finishing. (15:58)
비동기 (async) 함수의 경우
caller가 await로 async 함수인 callee를 호출하는 순간, callee는 일시 중지될 수 있다.
그러면 callee가 thread 제어권을 포기하면서 제어권은 System에게 넘어가게 된다.
(이러면 caller 또한 '일시 중지'가 된다.)
일시 중지된 함수는 System에게
“I know you have a lot of work to do. You decide what’s most important.” 라고 말하고, 이제 System은 Determine Priority를 통해 Other Tasks를 진행하게 된다.
그러다가 System이 중지되었던 async 함수를 다시 실행하는것이 중요하다고 판단하는 순간, 제어권을 callee에게 주고 마저 실행이된다.
(이때, System이 callee에게 전달되는 제어권은 다른 thread일 수도 있다.)
callee가 resume 되어 작업을 마치면 제어권을 다시 caller에게 되돌려준다.
즉, suspending을 통해 제어권을 포기한다. (Not thread-blocking!)
🗣️ "But unlike a normal function, it can give up control of the thread in an entirely different way: by suspending." (16:14)
async 함수는 일시 중지되는 동안 다른 일을 할 수 있기 때문에, await
키워드로 async
호출을 해야한다. 뿐만 아니라 일시 중지되는 동안 App의 state 크게 바뀔 수도 있다.
간단하게 정리하자면 아래와 같이 설명할 수 있다.
async
enables a function to suspend.await
marks where an async function may suspend excution.Vapor framework를 이용하여 localhost 서버를 구축하고 SwiftUI 환경에서 async/await 를 간단하게 적용해보았다. (+ 08. 06 vapor 추가)
Vapor Framework와 PostgreSQL 기반의 ORM인 Fluent를 사용하였고, Galaxy와 Star의 관계는 Parent-Children 즉, 1:N 관계를 형성시켜주었다.
디코딩할 데이터 모델을 정의하고 서버로부터 데이터를 가져오기 위한 fetchData()에 async 키워드로 비동기 함수임을 나타내주었다.
guard let url: URL = URL(string: "http://127.0.0.1:8080/galaxies") else { throw URLError(.badURL) }
var request: URLRequest = URLRequest(url: url)
request.httpMethod = "GET"
먼저 URL 생성과 유효성을 검사한 뒤, 주어진 url을 통해 URLRequest를 생성하고 데이터 조회를 목적으로 HTTP 메서드의 "GET" 방식으로 설정하였다.
let (data, response): (Data, URLResponse) = try await URLSession.shared.data(for: request)
data(for:)는 함수의 정의에 async 키워드가 붙은 비동기 함수로써, do-catch 블록에서 try await 키워드를 사용하여 비동기적으로 데이터를 요청하고 요청에 성공하면, 데이터와 응답 객체를 반환한다.
guard (200..<300).contains((response as? HTTPURLResponse)!.statusCode) else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode(Array<Galaxy>.self, from: data)
HTTPURLResponse를 통해 상태 코드가 200~299 범위에 있는지 HTTP 응답을 검증하고, 상태 코드가 범위 내에 존재하면 JSONDecoder를 사용하여 서버에서 받은 JSON 데이터를 디코딩한다.
Store 객체를 통해 값을 방출하고 방출된 값을 통해 List형식의 View를 아래와 같이 그려주었다.