경우에 따라 일시적인 네트워크 문제를 해결하거나 특정 형태의 연결을 다시 설정하기 위해 실패한 비동기 작업을 자동으로 다시 재시도해야 하는 경우가 있다.
struct SettingsLoader {
var url: URL
var urlSession = URLSession.shared
var decoder = JSONDecoder()
func load() -> AnyPublisher<Settings, Error> {
urlSession
.dataTaskPublisher(for: url)
.map(\\.data)
.decode(type: Settings.self, decoder: decoder)
.retry(3)
.eraseToAnyPublisher()
}
이것은 우리는 네트워크 호출을 구현하기 위해 Combine 프레임워크를 사용하여 발생한 오류를 처리하기 전에 최대 3번 재시도한 코드이다.
이 예시는 발생한 오류의 종류에 상관없이 로드 작업을 최대 3회까지 무조건 재시도한다.
Combine에선 Publisher 프로토콜에 포함된 retry 오퍼레이터가 있으나 Swift 의 concurrency 에선 비슷한 것을 제공하지 않기 때문에 만들어야 한다.
Swift의 concurrency 시스템에서 async/await 의 특별한 점 중 하나는 if 문 및 for 루프와 같은 표준 제어문과 다양한 비동기 호출을 혼합할 수 있다는 것이다.
struct SettingsLoader {
var url: URL
var urlSession = URLSession.shared
var decoder = JSONDecoder()
func load() async throws -> Settings {
// Perform 3 attempts, and retry on any failure:
for _ in 0..<3 {
do {
return try await performLoading()
} catch {
// This 'continue' statement isn't technically
// required, but makes our intent more clear:
continue
}
}
// The final attempt (which throws its error if it fails):
return try await performLoading()
}
private func performLoading() async throws -> Settings {
let (data, _) = try await urlSession.data(from: url)
return try decoder.decode(Settings.self, from: data)
}
}
따라서 await 마킹된 호출 (await performLoading()) 에 대한 자동 재시도를 구현하는 한 가지 방법은 실행하려는 비동기 코드를 반복하려는 루프 내에 재시도 횟수와 함께 배치하는 것이다.
이를 Task타입에 확장하여 재사용성이 더 높은 API를 추가할 수도 있다.
extension Task where Failure == Error {
@discardableResult
static func retrying(
priority: TaskPriority? = nil,
maxRetryCount: Int = 3,
operation: @Sendable @escaping () async throws -> Success
) -> Task {
Task(priority: priority) {
for _ in 0..<maxRetryCount {
try Task<Never, Never>.checkCancellation()
do {
return try await operation()
} catch {
continue
}
}
try Task<Never, Never>.checkCancellation()
return try await operation()
}
}
}
로직은 이전과 거의 동일하게 유지되지만, 최대 재시도 횟수를 매개변수화하고 취소 기능을 추가했다.
여기서 한 단계 더 나아가 볼 것이다.
비동기 작업을 재시도할 때 각 재시도 사이에 약간의 지연을 추가해준 것이다.
이는 재호출 전 다른 시스템(예: 서버)이 오류를 복구하기 위한 시간을 주기 위한 것일 수 있다.
이러한 지연을 지원하기 위해 Task.sleep 을 사용하였다.
extension Task where Failure == Error {
@discardableResult
static func retrying(
priority: TaskPriority? = nil,
maxRetryCount: Int = 3,
retryDelay: TimeInterval = 1,
operation: @Sendable @escaping () async throws -> Success
) -> Task {
Task(priority: priority) {
for _ in 0..<maxRetryCount {
do {
return try await operation()
} catch {
let oneSecond = TimeInterval(1_000_000_000)
let delay = UInt64(oneSecond * retryDelay)
try await Task<Never, Never>.sleep(nanoseconds: delay)
continue
}
}
try Task<Never, Never>.checkCancellation()
return try await operation()
}
}
}
이제 for 루프 시작 부분에 있는 checkCancellation을 제거할 수 있다. 그 이유는 Task.sleep 호출은 작업이 취소된 경우 자동으로 에러를 throw하기 때문이다.
필요에 따라 작업 클로저에 “semi-public”인 @_implicitSelfCapture 속성을 추가해 기본 Task 유형과 같은 암묵적인 self 캡처를 얻을 수 있다.
다만 밑줄 친 속성은 언제든지 변경될 수 있기 때문에 사용하지 않고 앞에 SettingLoader를 리팩터링할 것이다.
struct SettingsLoader {
var url: URL
var urlSession = URLSession.shared
var decoder = JSONDecoder()
func load() async throws -> Settings {
try await Task.retrying {
let (data, _) = try await urlSession.data(from: url)
return try decoder.decode(Settings.self, from: data)
}
.value
}
}
.value속성을 사용하여 return 된 값을 관찰하거나 작업 내에서 발생한 오류를 다시 throw 할 수 있다.
출처
Automatically retrying an asynchronous Swift Task | Swift by Sundell
Automatically retrying an asynchronous Swift Task | Swift by Sundell
Sometimes, we might want to automatically retry an asynchronous operation that failed, for example in order to work around temporary network problems, or to re-establish some form of connection. Here we’re doing just that when using Apple’s Combine fra
www.swiftbysundell.com