본문 바로가기

프로그래밍/iOS,macOS

dataTaskPublisher 재시도 및 출력 타입 변경

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)
}