retry(_ retries:)
상위 스트림을 다시 시도하는 메쏘드는 retry 이고 오류 발생시 해당 횟수 만큼 재시도 하게 된다.
retry 의 경우 이전 스트림을 다시 시도하므로, dataTaskPublisher 에 retry 를 걸게 되면 오류 발생시 바로 재시도를 수행한다. 더불어 URLError에 대해서만 retry 가 일어나게 된다.
dataTaskPublisher 로 데이터를 가져올때 단순 재시도가 아닌 일정시간 딜레이를 두어 재시도를 하는 케이스를 한번 살펴보자.
추가적인 에러 처리를 더하기 위해 별도 함수를 하나 추가한다. 이 메쏘드에서는 response 에 대한 추가적인 에러 처리와 Output을 Data 형식으로 변경했다.
URLError 이외에 http status code와 같은 비지니스 로직상의 오류도 추가로 재시도 하도록 위해 tryMap 을 통해 상황에 따른 오류들을 throw 할 수 있도록 한다.
extension URLSession {
func publisher(for request: URLRequest) -> AnyPublisher<Data, Error> {
return dataTaskPublisher(for: request)
.tryMap { data, response in
guard let httpResponse = response as? HTTPURLResponse else {
throw MyCustomError.case
}
guard 200..<300 ~= httpResponse.statusCode else {
throw MyCustomError.case
}
return data
}
.eraseToAnyPublisher()
}
}
재시도
dataTaskPublisher(:) 메쏘드가 아닌 위에 추가한 메쏘드를 호출하고, retry 전에 catch 로 에러를 핸들링 한다.
에러가 전달된 경우 delay를 통해 3초후에 Fail을 전달하도록 했다.
class MyCustomHttp {
func request(someURLRequest: URLRequest) -> AnyPublisher<Data, Error> {
return URLSession.shared.publisher(for: someURLRequest)
.catch { (error: Error) -> AnyPublisher<Data, Error> in
return Fail(error: error)
.delay(for: 3, scheduler: DispatchQueue.main)
.eraseToAnyPublisher()
}
.retry(2)
.eraseToAnyPublisher()
}
}
JSON 디코딩
dataTaskPublisher의 경우 Output과 Failure 가 아래와 같은데, 위에 tryMap을 통해 Output을 Data로 변경해 주었다.
public typealias Output = (data: Data, response: URLResponse)
public typealias Failure = URLError
Data 형식으로 변경한 이유는 제공되는 decode(type:,decoder:) 메쏘드를 사용하기 위함이다. request 메쏘드에 decode() 를 추가해 보자.
func request(someURLRequest: URLRequest) -> AnyPublisher<Decodable, Error> {
return URLSession.shared.publisher(for: someURLRequest)
.catch { (error: Error) -> AnyPublisher<Data, Error> in
return Fail(error: error)
.delay(for: 3, scheduler: DispatchQueue.main)
.eraseToAnyPublisher()
}
.retry(2)
.decode(type: CustomDecodableType.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
출력 타입 변경
단일 요청-응답의 경우 성공과 오류만 체크하게 되는데, Output과 Failure 가 분리되어 있다보니 매번 sink(completion:) 에서 Failure 처리를 하는 것도 번거롭게 느껴질 수 있다. 혹은 특정 변환 중에 guard 등으로 오류가 아닌 오류(?)를 정상 스트림으로 전달하고 싶은 경우도 있을 수 있다. 이런 경우에 swift 에서 자주 사용하는 Result로 출력을 변경해 사용한다.
스트림을 <Result<Decodable, Error>, Never> 형태로 변경해 준다.
데이터와 오류를 모두 Result 로 변경해 하위 스트림으로 전달하도록 수정. Failure는 Never.
func request(someURLRequest: URLRequest) -> AnyPublisher<Result<Decodable, Error>, Never> {
return URLSession.shared.publisher(for: someURLRequest)
.catch { (error: Error) -> AnyPublisher<Data, Error> in
return Fail(error: error)
.delay(for: 3, scheduler: DispatchQueue.main)
.eraseToAnyPublisher()
}
.retry(2)
.decode(type: CustomDecodableType.self, decoder: JSONDecoder())
.tryCompactMap { response -> Result<Decodable, Error> in
return Result.success(response)
}
.catch { (error: Error) -> AnyPublisher<Result<Decodable, Error>, Never> in
return Just(Result.failure(error)).eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
정상 데이터이던 오류이던 모두 정상 스트림으로 전달되고, Result 내에 오류 여부를 포함하게 된다.
이제 구독시 아래와 같이 receiveValue: 에서 사용하면 된다.
func testHttp() {
let expectation = XCTestExpectation()
myCustomHttp.request(someURLRequest: request)
.sink { result in
switch result {
case .success(let value):
expectation.fulfill()
case .failure(let error):
XCTFail(error.localizedDescription)
}
}
wait(for: [expectation], timeout: 2.0)
}
'프로그래밍 > iOS,macOS' 카테고리의 다른 글
[concurrency] Task, TaskGroup, task timeout (0) | 2023.02.28 |
---|---|
[swift] sorted array 에 값 추가 (0) | 2022.06.10 |
[concurrency] swift async/await (0) | 2022.05.13 |
[Combine] 콜백 기반 여러 처리 결과를 배열로 받기 (0) | 2022.03.20 |
[SwiftUI] 오디오 레벨 에니메이션, Shape (0) | 2022.01.06 |
Framework SPM 배포 (0) | 2021.09.24 |
URLSession.DataTaskPublisher (0) | 2021.09.17 |
카메라 데이터 수신을 위한 AVCaptureSession (0) | 2021.08.02 |
collection view 에서 load more 처리 (0) | 2021.07.20 |
UIPanGestureRecognizer 슬라이드 다운 뷰 (0) | 2021.07.01 |