GCD (Grand Centeral Dispatch)
일반적으로 네트워크 통신을 할 때 우리는 비동기적으로 코드를 작성한다.
다음과 같이 비동기적으로 요청을 하여 응답 결과를 call back 받기 위해 completion handler를 통해 받아오고, 받아온 데이터를 화면에 반영한다.
func fetchThumbnailURLSession(completion: @escaping (Result<UIImage, JackError>) -> Void) {
let url = URL(string: "https://www.themoviedb.org/t/p/w600_and_h900_bestv2/7M2pc9OboapgtoBbkU49Aim7O5B.jpg")!
let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 5)
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data else {
completion(.failure(.unknown))
return
}
guard error == nil else {
completion(.failure(.unknown))
return
}
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
completion(.failure(.invalidResponse))
return
}
guard let image = UIImage(data: data) else {
completion(.failure(.invalidImage))
return
}
completion(.success(image))
}.resume()
}
GCD는 위와 같이 주로 사용되고 있는 동시성 프로그래밍 방식이다.
그렇다면 Swift Concurrency는 왜 등장한 것일까?
Swift Concurrency
GCD의 문제점
GCD를 이용하여 비동기적으로 구현하게 되면 여러 작업을 수행하기 위해 여러 개의 스레드가 생성된다.
여러 스레드가 마치 동시에 수행되고 있는 것 처럼 보이지만 CPU의 코어는 하나의 프로세스만 수행할 수 있기 때문에 컨텍스트 스위칭을 통해 여러 작업을 빠르게 이동하면서 처리하는 것이다.
CPU에서 실행 할 프로세스를 교체하는 것을 컨텍스트 스위칭(Context Switching) 이라고 한다.
비동기 방식으로 많은 작업을 수행하게 되면 여러 개의 스레드가 생성될 것이고, 스레드가 많아질 수록 생성된 스레드 수 만큼 컨텍스트 스위칭이 발생하게 된다.
필요 이상의 스레드가 생성되어(Thread Explosion) 과도한 컨텍스트 스위칭(Excessive Context Switching)으로 인해 스케줄링 오버헤드가 발생하게 된다.
Swift Concurrency
Swift Concurrency는 이러한 GCD Queue의 문제점을 해결하기 위한 특징을 가지고 있다.
- CPU 코어의 수와 최대로 생성되는 스레드의 수를 같게 한다.
- 같은 스레드 내에서 continuation 전환 형식으로 방식으로 변경
- 스레드간 컨텍스트 스위칭이 없다.
async throws / try await: 비동기를 동기처럼
✅ Continuations
Swift Concurrency에서는 await 키워드를 만나게 되면 시스템에게 스레드 제어 권한을 넘겨(Suspend) 시스템 내부에서 비동기로 처리하도록 한다.
시스템이 작업을 완료하게 되면 비동기 함수에게 스레드 제어 권한을 다시 전달하고, 받은 시점에 남아있는 스레드에게 나머지 작업을 수행하도록 넘겨 작업을 재개(resume)한다.
그래서 continutaion이 뭐야?
Continuation은 await 이후에 스레드 제어권을 포기하고 다시 재개하는 그 일련의 과정을 말한다.
Continuation은 이러한 과정에서 시스템에게 제어 권한을 넘길 때 해당 시점에 사용하던 데이터를 저장해두고, 다시 재개할 때 일시 정지된 상태의 함수를 추적하여 어디서부터 재개할 수 있는지 알 수 있도록 한다.
Async Await
Swift Concurrency의 핵심적인 문법인 async / await에 대해 코드를 통해 알아가보자!
async
네트워크 통신을 하기 위해 비동기로 작업할 메서드에 async 키워드를 붙여준다.
async가 붙어있다? 이 메서드는 비동기로 동작 할 것이다! 선언하는 것.
Swift Concurrency를 사용할 것이기 때문에 기존에 URLSession에서 사용하던 dataTask 메서드가 아니라 URLSession이 가지고 있는 메서드들 중에 swift concurrency로 동작하는 메서드를 선택
URLSession은 비동기로 동작한다.
async가 붙어있는 메서드를 사용하기 위해서는 await 키워드를 통해 응답 결과가 도착할 때 까지 기다리도록 해야한다.
await
await 키워드를 만나게 되면 작업을 받은 스레드가 시스템이 알아서 작업하도록 스레드에 대한 제어권을 포기한다.try await
키워드를 사용함으로서 비동기를 동기처럼 작업할 것임을 선언하여 응답이 올 때 까지 await 시점에서 기다리도록 한다.
class Network {
static let shared = Network()
private init() { }
// 비동기로 동작
func fetchThumbnailAsyncAwait() async throws -> UIImage {
let url = URL(string: "https://www.themoviedb.org/t/p/w600_and_h900_bestv2/7M2pc9OboapgtoBbkU49Aim7O5B.jpg")!
let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 5)
// await: 비동기를 동기처럼 작업할 것이니까, 응답 올 때 까지 기다려라
let (data, response) = try await URLSession.shared.data(for: request)
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
throw JackError.invalidResponse
}
guard let image = UIImage(data: data) else {
throw JackError.invalidImage
}
return image
}
}
네트워크 통신 결과를 사용해보자
viewDidLoad()
는 당연히도 동기로 동작하는 메서드이다. 동기로 동작하는 메서드 내에서 비동기 메서드를 호출할 수 없다. 비동기로 동작하기로 약속을 해버렸으니까..!
Task
Task를 통해 비동기로 동작할 수 있도록 설정해줘야 한다.
Task는 비동기 메서드와 동기 메서드를 연결해주는 역할을 한다.
DispatchQueue.global.async
를 통해 비동기로 동작하도록 하는 것과 비슷하다.
우리는 Swift Concurrency를 사용할 것이기 때문에 Task를 사용!
Task {
let image = try await Network.shared.fetchThumbnailAsyncAwait()
posterImageView.image = image
}
async await으로 구현된 메서드이기 때문에 값을 전달 받을 때 까지 기다리도록 await을 붙여준다.
여러 이미지를 받아온다면 기존 비동기로 받아오는 방식과 다르게 순서대로 이미지를 받아오게 된다.
image1을 받아올 때 까지 기다리기 때문에 image2가 더 짧은 시간이 걸린다고 하더라도 image1의 통신이 완료된 후 요청을 하게 된다.
순서가 중요하지 않다면....
Async let
async await을 사용하면 하나의 작업을 마무리할 때 까지 기다린 후 완료되면 다음 작업을 수행한다.
무조건 요청한 순서대로 응답 결과를 받게 된다.
여러 네트워크 통신 작업을 한 번에 수행하고 한 번에 응답 결과를 받고 싶다면 Async let을 사용해야 한다.
Dispatch Group과 비슷한 기능
func fetchThumbnailAsyncLet() async throws -> [UIImage] {
async let image1 = try await Network.shared.fetchThumbnailAsyncAwait(value: "aZuBfbR0PnCb2up7lqHDsgJlLjs")
async let image2 = try await Network.shared.fetchThumbnailAsyncAwait(value: "lMWTlGr9jVUC18T515hPRKym5QQ")
async let image3 = try await Network.shared.fetchThumbnailAsyncAwait(value: "7M2pc9OboapgtoBbkU49Aim7O5B")
return try await [image1, image2, image3]
}
async let
키워드를 사용하여 여러 작업을 한 번에 처리하도록 구현한다.
비동기 작업 구문을 비동기 작업 처리를 하기 때문에 리턴할 때 await을 붙여 모든 작업이 완료되길 기다린 후 리턴하도록 한다.
응답 순서가 다른 것을 볼 수 있다.
async let의 한계
명확하게 네트워크 호출을 해야하는 수를 알 때만 사용할 수 있다.
그룹으로 묶기 위해 각각 네트워크 호출을 해야한다.
TaskGroup
몇 개의 작업이 전달될 지 모를 때 사용한다.
withThrowingTaskGroup()
응답 받을 데이터의 타입 (UIImage.self)
네트워크 통신이 몇 번 일어날 지 모르기 때문에 withThrowingTaskGroup
내부에서 요청 할 task를 group.addTask
를 통해 추가하여 하나의 task group을 생성한다.
group에 task를 추가하기 때문에 group 내부에 네트워크 통신 응답 값을 가지고 있다.
응답 값을 얻기 위해 group에서 꺼내와야 한다.
group 또한 비동기로 작동하기 때문에 반복문을 수행할 때 await 키워드를 붙여준다.
func fetchThumbnailTaskGroup() async throws -> [UIImage] {
let poster = ["aZuBfbR0PnCb2up7lqHDsgJlLjs", "lMWTlGr9jVUC18T515hPRKym5QQ", "7M2pc9OboapgtoBbkU49Aim7O5B"]
return try await withThrowingTaskGroup(of: UIImage.self) { group in
for item in poster {
group.addTask { // 네트워크 통신이 몇 번 일어날 지 모르니까 그룹으로 묶기 위해 addTask
try await self.fetchThumbnailAsyncAwait(value: item)
}
}
// group에 addTask를 하기 때문에 응답 값 또한 group이 가지고 있다.
var resultImages: [UIImage] = []
for try await item in group {
resultImages.append(item)
}
return resultImages
}
}
결과
여러 번의 네트워크 통신을 수행해야 하고, 순서가 중요하지 않을 때 TaskGroup을 사용한다.
참고
'iOS > 🔎 swift 정리하기' 카테고리의 다른 글
[iOS/Kingfisher] 네트워크 통신으로 이미지 받아오기 (0) | 2023.11.26 |
---|---|
[iOS/RxSwift] CombineLatest vs Observable.zip (0) | 2023.11.19 |
RxSwift 정리하기 (0) | 2023.11.08 |
[iOS/Swift] MapKit Annotation displayPriority 지정하기 (0) | 2023.10.20 |
MapKit CustomAnnotation (0) | 2023.10.10 |