본문 바로가기

카테고리 없음

Discover Concurrency (4) - Asynchronous Task 자동 재실행 시키기

경우에 따라 일시적인 네트워크 문제를 해결하거나 특정 형태의 연결을 다시 설정하기 위해 실패한 비동기 작업을 자동으로 다시 재시도해야 하는 경우가 있다.

 

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