Task
는 비동기 작업을 나타내는 개념적인 단위로, 비동기 코드를 작성하고 실행하기 위한 핵심 요소 중 하나입니다.
async/await
를 사용하면 비동기 코드를 동기 코드와 유사한 방식으로 작성할 수 있어 가독성이 향상되며, 오류 처리도 간편해집니다. 또한, 비동기 작업을 효율적으로 처리할 수 있습니다.
하지만 비동기 코드에도 문제점이 있습니다. 아래의 코드와 실행 결과를 보고 어떤 문제점이 있는지 찾아보겠습니다.
@objc func download() {
Task {
let firstData = await donwnLoadImage(imageUrl: ImageURL.first)
imageView1.image = UIImage(data: firstData)
let secondData = await donwnLoadImage(imageUrl: ImageURL.second)
imageView2.image = UIImage(data: secondData)
let thirdData = await donwnLoadImage(imageUrl: ImageURL.third)
imageView3.image = UIImage(data: thirdData)
}
}
func donwnLoadImage(imageUrl: ImageURL) async -> Data {
let dataTask = AF.request(imageUrl.url, method: .get)
.downloadProgress(closure: { progress in
switch imageUrl {
case .first:
self.progressView1.progress = Float(progress.fractionCompleted)
case .second:
self.progressView2.progress = Float(progress.fractionCompleted)
case .third:
self.progressView3.progress = Float(progress.fractionCompleted)
}
})
.serializingData()
switch await dataTask.result {
case .success(let data):
return data
case .failure(let error):
print(error.localizedDescription)
}
return Data()
}
어떤 문제점이 있는지 찾으셨나요?
위의 코드는 Serial(직렬)
하게 작동되는 형태입니다. Task를 사용하여 세 개의 이미지를 비동기적으로 다운로드하고 화면에 표시하고 있지만 각 이미지를 다운로드할 때 순차적으로 처리하고 있기 때문에 성능이 좋지 않습니다.
(각각의 사진을 다운로드하는데 걸리는 시간이 3초, 7초, 10초라고 할 때 Serial(직렬) 방법을 사용하면 3초 + 7초 + 10초 = 20초가 걸리는 비효율적인 코드입니다.)
이러한 성능을 개선하기 위해서 이미지 다운로드 작업을 Concurrency(동시)로 실행하면 효율적으로 이미지를 다운로드할 수 있습니다.
@objc func download() {
Task {
async let firstData = donwnLoadImage(imageUrl: ImageURL.first)
async let secondData = donwnLoadImage(imageUrl: ImageURL.second)
async let thirdData = donwnLoadImage(imageUrl: ImageURL.third)
imageView1.image = await UIImage(data: firstData)
imageView2.image = await UIImage(data: secondData)
imageView3.image = await UIImage(data: thirdData)
}
}
func donwnLoadImage(imageUrl: ImageURL) async -> Data {
// 생략
}
async let
을 사용하여 이미지를 다운로드하면 세 개의 이미지를 동시에 다운로드하는 것을 볼 수 있습니다. (각각의 사진을 다운로드하는데 걸리는 시간이 3초, 7초, 10초라고 할 때 Concurrency(동시) 방법을 사용하면 이미지 최대 다운로드 시간인 10초가 걸리므로 Serial(직렬) 방법에 비해 압도적인 시간 차이가 나는 것을 볼 수 있습니다.)
하지만! 해당 코드를 자세히 보면 문제점을 또 찾을 수 있습니다.
async let
을 사용해서 동시에 이미지를 다운로드하지만 imageView에 다운로드한 image를 업데이트하는 과정에서 await를 사용하기 때문에 두 번째, 세 번째 이미지를 먼저 다운로드했더라도 첫 번째 사진을 다운로드하는 중이라면 첫 번째 사진을 업데이트할 때까지 기다려야 하는 문제가 있습니다.
(실행 영상에서도 두 번째 이미지를 먼저 다운로드했음에도 불구하고 첫 번째 이미지를 다운로드할 때까지 기다리는 것을 볼 수 있습니다.)
문제 해결을 위해 어떤 방법을 사용해야 할까요? 아래에서 코드와 함께 알아보겠습니다.
문제 해결을 위해 아래와 같이 이미지를 요청하고 업데이트하는 과정을 하나의 Task로 묶어서 세 개의 Task가 병렬로 실행되도록 만들었습니다.
각각의 Task는 이미지 데이터를 비동기로 다운로드하고 이를 해당 이미지 뷰에 표시합니다.
이렇게 함으로써 애플리케이션의 성능을 향상시키고 여러 이미지를 효과적으로 처리할 수 있게 만들었습니다.
@objc func download() {
Task {
async let firstData = donwnLoadImage(imageUrl: ImageURL.first)
imageView1.image = await UIImage(data: firstData)
}
Task {
async let secondData = donwnLoadImage(imageUrl: ImageURL.second)
imageView2.image = await UIImage(data: secondData)
}
Task {
async let thirdData = donwnLoadImage(imageUrl: ImageURL.third)
imageView3.image = await UIImage(data: thirdData)
}
}
func donwnLoadImage(imageUrl: ImageURL) async -> Data {
// 생략
}
실행 결과를 보면 다운로드 완료된 순서대로 UIImage를 업데이트해 주는 것을 볼 수 있습니다!
하지만 만약에 이미지가 100개라면??? Task를 100개 만들어서 이미지를 다운로드하기에는 비효율적이게 됩니다. 따라서 이를 해결하기 위해 아래에서 TaskGroup
을 코드와 함께 설명드리겠습니다.
TaskGroup
은 비동기 프로그래밍에서 여러 비동기 작업을 그룹화하고 관리하기 위한 편리한 방법을 제공합니다.
즉, Task Group을 사용하면 여러 비동기 작업을 동시에 실행하고 결과를 수집하거나 에러를 처리할 수 있습니다.
Task {
let dataList = await getData()
imageView1.image = UIImage(data: dataList[0])
imageView2.image = UIImage(data: dataList[1])
imageView3.image = UIImage(data: dataList[2])
}
func getData() async -> [Data] {
let imageUrls: [ImageURL] = [.first, .second, .third]
let datas = await withTaskGroup(of: (Data, Int).self) { group in
for (index, imageUrl) in imageUrls.enumerated() {
group.addTask {
let data = await self.donwnLoadImage(imageUrl: imageUrl)
return (data, index) // 데이터와 인덱스를 함께 반환
}
}
var dataList: [Data?] = Array(repeating: nil, count: imageUrls.count)
for await (data, index) in group {
dataList[index] = data
}
return dataList.compactMap { $0 } // nil 값을 제거하여 순서대로 정렬된 배열 반환
}
return datas
}
func donwnLoadImage(imageUrl: ImageURL) async -> Data {
// 생략
}
Task Group의 종류에는 여러 가지가 있지만 위의 코드에서는 withTaskGroup
을 사용했습니다.
withTaskGroup 함수를 사용하여 이미지 데이터 다운로드 작업을 관리하기 위한 Task Group을 생성하여 이미지를 다운로드하는 비동기 작업을 그룹에 추가하는 방식입니다.
이미지를 다운로드하는 작업마다 Task를 생성하는 방식과 동작은 같지만 withTaskGroup
을 사용하면
효율적인 코드를 작성할 수 있습니다.
⭐️ 에러가 날 수 있는 상황이라면 withTaskGroup
대신 withThrowingTaskGroup
를 사용하면 됩니다.