안녕하세요. 후루륵짭짭입니다.
이전에 Async Await에 대해서 체험판으로 정리한 적이 있었습니다.
2022.08.13 - [Xcode/Swift - PlayGround] - PlayGround) Async - Await 경험 정리#1
이때는 정말 체험판으로 작성 했다면, 이제는 좀 더 심도 있게 경험한 것들을 차례대로 정리나가려고 합니다.
정말 기본적인 것은 위에 정리하였고 오랜만에 다시 공부하면서 정리해보려고 합니다.
** Async - Await에 대한 정리 **
func calculateAPR(creditScores : [CreditScore]) -> Double {
let sum = creditScores.reduce(0, {next, credit in
return next + credit.score
})
return Double( (sum/creditScores.count) / 100)
}
func getAPR(userId: Int) async throws -> Double {
guard let equifaxUrl = Constants.Urls.equifax(userId: userId),
let experianUrl = Constants.Urls.experian(userId: userId) else {
throw NetworkError.badUrl
}
//일반적인 await는 해당 메소드가 끝나고 나서 다음 것을 수행합니다.
let (equifaxData, _) = try await URLSession.shared.data(from: equifaxUrl)
print("equifaxData : \(equifaxData)")
let (experianData, _) = try await URLSession.shared.data(from: experianUrl)
print("experianData : \(experianData)")
return 0.0
}
Await 함수는 해당 과정이 끝나고 나서 다음 단계로 넘어 갑니다.
따라서 eqifaxData의 Await가 끝나고 나서 experianData를 수행하고 이 experianData가 끝나야 return을 하게 됩니다.
그리고 당연하게 Await는 비동기인 Async에서 작동해야하죠.
그런데 만약 절차적으로 수행하기 보다는 병렬적으로 작업하고 싶은 경우에는 async let을 사용했습니다.
func getAPRAsynclet(userId: Int) async throws -> Double {
if userId % 2 == 0 {
throw NetworkError.invalidId
}
try Task.checkCancellation() //부모나 다른 Task들이 취소되었다면 더 이상 해당 로직을 수행할 필요가 없기 때문에 에러를 발생시킨다.
guard let equifaxUrl = Constants.Urls.equifax(userId: userId),
let experianUrl = Constants.Urls.experian(userId: userId) else {
throw NetworkError.badUrl
}
//Async let을 사용하면 순서에 상관없이 먼저 온 것을 수행한다.
(1) async let (equifaxData, _) = URLSession.shared.data(from: equifaxUrl)
(2) async let (experianData, _) = URLSession.shared.data(from: experianUrl)
//async let 이기 때문에 일단 비동기 적으로 수행을 하고 try await가 아래에 있기 때문에 해당 구문을 수행하기 전에는 experianData 결과가 있어야한다.
(3) let experianCreditScore = try JSONDecoder().decode(CreditScore.self, from: try await experianData)
print("experianCreditScore : \(experianCreditScore)")
(4) let equifaxCreditScore = try JSONDecoder().decode(CreditScore.self, from: try await equifaxData)
print("equifaxCreditScore : \(equifaxCreditScore)")
return calculateAPR(creditScores: [experianCreditScore,equifaxCreditScore ])
}
이렇게 하면 (1) 끝나고 (2)를 수행하는 것이 아닌 (1)과 (2)가 동시에 작동을 합니다.
그리고 (3)에서 experianData 작업이 끝났는지 확인하고 해당 작업이 끝나면 (4)으로 넘어가고 이 또한 끝나야 return을 하게 됩니다.
** Group Task **
기존에 사용하던 비동기 처리 함수 중 DispatchGroup이라는 것이 존재 했습니다.
이는 여러 비동기 처리를 기다렸다가 한번에 처리할 때 사용하는 겁니다.
그런데 이것과 동일한 기능을 Async Await에서도 제공합니다.
struct ReturnValue : Sendable {
let id : Int
let score : Double
}
Task(operation: {
var resultValues : [ReturnValue] = []
for id in [1,2,3,4,5] {
do{
//이렇게 내부는 Concurrency로 작동하지만 상위가 Serial이라면 반복문읜 Serial로 작동한다.
let apr = try await getAPRAsynclet(userId: id)
resultValues.append(.init(id: id, score: apr))
}
catch{
resultValues.append(.init(id: id, score: 0))
}
}
print(resultValues)
})
이렇게 반복문안에 작업을 해도 결국에 한번에 모았다가 처리하게 됩니다.
왜냐하면 하나의 await 작업이 끝나야 다음 await 작업을 진행하기 때문입니다.
이렇다고 한다면 순차적으로 결과를 받아오기는 하지만 데이터 처리 속도가 늦어질 수 있습니다.
FOR RESULT
[UnstructuredConcurrencyIntro.ReturnValue(id: 1, score: 7.0),
UnstructuredConcurrencyIntro.ReturnValue(id: 2, score: 0.0),
UnstructuredConcurrencyIntro.ReturnValue(id: 3, score: 8.0),
UnstructuredConcurrencyIntro.ReturnValue(id: 4, score: 0.0),
UnstructuredConcurrencyIntro.ReturnValue(id: 5, score: 7.0)]
위와 같이 1,2,3,4,5 순차적으로 결과가 나오게 됩니다.
반면 withTaskGroup이라는 것이 존재합니다.
func getAPRForAllUsers() async throws -> [ReturnValue] {
var resultValues : [ReturnValue] = []
try await withThrowingTaskGroup(of: ReturnValue.self, body: { group in
for id in [1,2,3,4,5] {
group.addTask(operation: {
do {
let result = try await self.getAPRAsynclet(userId: id)
return .init(id: id, score: result)
}
catch {
return .init(id: id, score: 0)
}
})
}
for try await result in group {
resultValues.append(result)
}
})
return resultValues
}
이는 DispatchGroup과 동일합니다.
DispatchGroup도 group.enter 와 group.leave를 통해서 모음을 만들어 줍니다 .
이것도 addTask로 수행할 작업을 넣어주고
마지막에 group을 await로 기다리면 해당 결과가 반환이 됩니다 .
TASKGROUP RESULT
[UnstructuredConcurrencyIntro.ReturnValue(id: 2, score: 0.0),
UnstructuredConcurrencyIntro.ReturnValue(id: 4, score: 0.0),
UnstructuredConcurrencyIntro.ReturnValue(id: 3, score: 7.0),
UnstructuredConcurrencyIntro.ReturnValue(id: 1, score: 7.0),
UnstructuredConcurrencyIntro.ReturnValue(id: 5, score: 6.0)]
그렇다면 위와 같이 결과가 출력이 되는데 순서가 보장되지 않습니다.
따라서 병렬적으로 작업하고 모든 과정이 다 끝나면 결과를 반환하게 됩니다 .
여기서 Sendable 프로토콜의 개념이 등장하게 됩니다.
withTaskGroup(of : sendableProtocol)이 들어가게 되는데,
SendableProtocol은 값이 비동기적으로 작업할 때 Thread Safe(Data race 방지)하게 값을 전달하기 위해서 존재하는 겁니다.
따라서 보통 Struct와 같은 Value 타입에 적용이 됩니다.
자세한 내용은 여기에 있습니다.
https://zeddios.tistory.com/1305
** Unstructured Concurrency Task **
지금 까지 배운 것은 절차적으로 수행 되는 Structured Concurrency라고 불리더라구요.
반면에 절차적으로 수행 되지 않고 사용자의 필요에 따라서 취소가 가능하고 호출해주는 Unstructured Concurrency인
Task라는 것이 존재합니다 .
위에서도 이미 예시로 나왔는데, async 함수를 호출해주기 위해서는 Task 공간 안에서 호출이 되어야 합니다.
Task는 사용자의 필요에 따라 결과 값을 가져올 수 있습니다.
var aprTask : Task<[ReturnValue],Error>? {
didSet {
if aprTask == nil {
APRResult.text = "Download"
}
else {
APRResult.text = "Canceled"
}
}
}
예를들어 위와 같이 Task를 에러가 존재하면 결과 값으로 [ReturnValue] 값을 반환하는 property를 설정한다면
aprTask = Task(operation: {
let result = try await self.getAPRForAllUsers()
return result
})
이렇게 Task Property에 Task 작업을 저장할 수 있게 됩니다.
Task(operation: {
do{
let result = try await aprTask?.value
print("TASKGROUP RESULT")
print(result ?? [])
}
catch {
print("resultError : ",error.localizedDescription)
}
})
그리고 위와 같이 await를 통해서 저장한 Task를 호출해주고 작업이 끝날 때 까지 기다린다음 value를 통해서 값을 가져옵니다.
만약 해당 Task를 취소하고 싶다면 아래 처럼 취소가 가능합니다.
aprTask?.cancel()
이렇게 Task는 사용자의 필요에 따라서 자유롭게 호출 및 취소가 가능하여 Unstructured Concurrency 라고 불린다고 합니다.
지금까지 그냥 입맛대로의 Async - Await에 대해 정리해봤습니다.
알아 볼 수 있을지 모르겠지만 ㅠㅠ
도움이 됐으면 합니다.
** 참고 사이트 **
https://sujinnaljin.medium.com/swift-actor-%EB%BF%8C%EC%8B%9C%EA%B8%B0-249aee2b732d
병렬적으로 작동하는 detached-task
https://www.avanderlee.com/concurrency/detached-tasks/
Sendable에 대한 정리
https://zeddios.tistory.com/category/Swift/Concurrency
StructuredConcurrency
https://www.andyibanez.com/posts/structured-concurrency-with-group-tasks-in-swift/
https://www.andyibanez.com/posts/structured-concurrency-in-swift-using-async-let/
https://www.andyibanez.com/posts/unstructured-concurrency-with-detached-tasks-in-swift/
'Xcode > Swift - PlayGround' 카테고리의 다른 글
PlayGround) Actor에 대해 경험한 것 적어보기(feat: Task, Async Await) (2) | 2023.08.13 |
---|---|
Swift) AsyncStream 정리하기 (feat: Sequence, IteratorProtocol) (0) | 2023.07.24 |
PlayGround) PropertyWrapper와 Dependency Injection (0) | 2023.01.23 |
PlayGround) Combine 체험기#2 (0) | 2023.01.08 |
PlayGround) Framework UnitTest 생성 해보기 (0) | 2022.12.17 |
댓글