얘는 .map, filter
와 같은 연산자가 값에 대해 수행하는 작업을 오류(즉, 실패)에 대해 수행하는 연산자를 의미합니다. 값과 오류는 모두 파이프라인을 따라 내려오는 객체이며 각각 타입이 있습니다.
한 값을 다른 값으로, 심지어 다른 타입으로 변환하거나 값이 파이프라인에서 이동 못하도록 할 수 있는 것처럼 오류에 대해서도 같은 종류의 작업을 할 수 있습니다.
오류 핸들러 중 가장 뛰어난 것은 .mapError
라고 하네요. .map
이 하는 일을 똑같이 수행하지만 오류에 대해서는 업스트림에서 오류를 수신하면 다른 오류 개체, 심지어 다른 타입의 오류 객체를 포함하는 오류를 다운스트림으로 보낼 수 있습니다.
.mapError (Publishers.MapError)
는 입력이 Error
인 맵함수를 받습니다. 일반적으로 이 연산자는 아무 작업도 하지 않고 위에서 받은 값을 전달할 뿐입니다. .finished
를 받으면 해당 값도 함께 전달함니다. 그러나 .failure
를 받으면 실패에 포함된 오류를 가져와서 전달합니다. 맵 함수는 오류를 반환해야 하며, 이 연산자는 오류를 새 .failure
로 감싸서 파이프라인으로 전달함니다.
얘는 다운스트림으로 전달되는 오류의 세부사항을 변경할 수 있을 뿐만 아니라 오류 타입 자체를 변경할 수도 있습니다 예를 들어 URLError.cancelled
를 수신한 경우 얘는 URLError.cannotConnectToHost
로 대체하거나, 더 나아가 아예 다른 에러타입으로도 대체가능함니다. 따라서 이 연산자는 파이프라인의 다운스트림 부분과 관련된 Failure
타입을 변경할 수 있습니다.
.tryMap
은 오류가 Error
를 만족하는 것만 알고 있기 때문에 다운스트림 부분에서 실패 타입을 더 정확하게 보려면 .mapError
를 사용할 수 있습니다.
.tryMap {
...
}
.mapError { $0 as! URLError }
참고로 업스트림 실패 타입이 Never
인 경우는 .mapError
를 사용할 수 없습니다. 대신 .setFailureType
을 사용하시믄댐니다.
.replaceError(Publishers.ReplaceError
는 다운스트림 파이프라인으로 전달되는 오류 타입을 업스트림의 오류 타입이 무엇이든 간에 Never
로 변경함니다. 이는 업스트림에서 오류가 발생할 경우 값으로 출력될 업스트림의 출력 타입의 값을 지정하여 수행됨니다.
URLSession.shared.dataTaskPublisher(for: url)
.map {$0.data}
.replaceError(with: Data())
.map
을 통과할 때까지 파이프라인의 출력 타입은 Data
이고 실패 타입은 URLError
임니다. 이제 위 연산자를 사용해서 위에서 오류가 발생하면 이를 차단하고 대신 자체 Data()
를 반환해야 한다고 말함니다. 이렇게 하면 다운스트림에 오류가 발생하지 않음을 보장할 수 있으므로 오류 타입은 Never
가 됨니다.
.setFailureType (Publishers.SetFailureType)
은 업스트림 오류 타입이 Never
인 경우 다운스트림 파이프라인의 오류 타입을 일종의 Error
로 변경하는 친구임니다.
try
로 시작하는 연산자를 사용하는 경우에는 이 친구를 굳이 사용할 필요는 없습니다. 위에 실패 유혀이 Never
인 경우에 주로 사용하고, 아래 다운스트림의 에러 타입을 맞추기 위해서도 주로 사용합니다.
self.myButton.publisher()
.setFailureType(to: Error.self)
.flatMap { _ in checkAccess().publisher }
.catch (Publishers.Catch)
는 .mapError
와 마찬가지로 실패가 파이프라인을 따라 내려오지 않는 한 호출되지 않는 맵 함수를 받습니다. 실패가 내려오면 업스트림에서 받은 모든것을 아래로 전달함니다. 이 함수가 호출되면 failure's error
를 매개변수로 받슴니다.
.replaceError
와 마찬가지로 맵함수는 값을 반환하고 다운스트림 실패 타입을 Never
로 변환할 수 있습니다.
.flatMap
과 마찬가지로 맵 함수가 생성하는 것은 퍼블리셔이며, 해당 게시자는 유지되어 게시를 시작하고 해당 게시자가 생성하는 값이 다운스트림으로 진행됩니다. 퍼블리셔의 출력 타입은 업스트림의 출력 타입과 일치해야 합니다.
아래 예제에서, url 하나가 잘못되어 전체 파이프라인이 실패하는것을 원치 않을떄 .replaceError
대신 .catch with just
를 사용할 수도 있습니다.
urls.map(URL.init(string:)).compactMap{$0}.publisher
.flatMap { url in
URLSession.shared.dataTaskPublisher(for: url)
.catch {_ in Just((data:Data(), response:URLResponse()))}
}
어떤 의미에서 .replaceError
는 .catch
의 단순한 경우에만 사용할 수 있는 편의 기능임니다. 하지만 .catch
는 훨씬 더 강력합니다. 이미 실패한 업스트림을 대체하여 완전히 새로운 파이프라인을 반환할 수 있기 때문임니다.
예시로, 2인용 게임에서 플레이어1 이 한동안 점수를 쌓다가 실수를 저질러 플레이어2 에게 플레이가 넘어가고, 플레이어 2는 점수를 낮춰야 하는 상황임니다.
var player1 = PassthroughSubject<Int,MyError>()
var player2 = PassthroughSubject<Int,MyError>()
@Published var total = 0
// 만약 오류를 범하면 아래를 호출함니다.
self.player1.send(completion: .failure(MyError.lostControl))
위 오류가 발생하면, 플레이어를 변경해야하는 신호입니다. .catch
를 사용하면 구현할 수 있슴니다! 파이프라인은 처음에 player1에 구독된 사애로 시작하지만, 오류가 발생하면 player2에 구독된 상태로 전환됩니다
let pub = self.player1
.catch {_ in self.player2 }
.catch {_ in Empty<Int,Never>() }
.map {self.total + $0}
.assign(to: \.total, on: self)
위에서 .catch
를 하나 더 쓴 경우는, 오류 타입을 Never
로 보장하기 위해서이고 이는 .assign
을 사용할 수 있게 해줌니다.
만약 플레이어2 도 오류를 발생해서 플레이어1로 돌아가고 싶다고 할 때, 위 예제에선 이미 플레이어1 이 취소가 되어서 직접 그렇게 하는건 불가능합니다. 플레이어1의 서브젝트를 대체하여 전체 파이프라인을 다시 생성할 수는 있슴니다.
이런 경우를 위해서 catch 내부에서 함수를 실행하던가 하여 변경시키고, 해당 퍼블리셔는 취소하고 빈 값만 보내는게 나을거 같다. 더 깔끔한 방법이 있을수도 있겠지만 당장은 이렇게 할듯,,?
또 tryCatch (Publishers.TryCatch)
도 존재함니다. 얘의 맵 함수는 thorws
할 수 있으므로 .catch
와 달리 다운스트림의 실패 타입을 Never
로 변경 할 수 없습니다. 맵 함수가 오류를 던지면 아래에 실패로 전달되고 연산자 자체가 취소됨니다.
.retry (Publishers.Retry)
는 Int 매개변수를 받아 저장함니다. 업스트림에서 파이프라인을 통해 실패가 발생하면 얘는 실패를 다운스트림에 전달하지 않고 받은 Int 값을 감소시킨다음 스스로 구독을 취소하고 업스트림을 다시 구독함니다 이 과정은 Int 값이 0이 아니면 계속될 수 있슴니다. Int가 0이고 업스트림에서 실패가 발생하면 이제 이 연산자는 실패를 다운스트림에 보냄니다.
// retry는 지연시간을 지정할 방법이 없으므로
// delay를 삽입해줘야함니다
URLSession.shared.dataTaskPublisher(for:url)
.retry(3)
let pub = URLSession.shared.dataTaskPublisher(for: url)
.delay(for: 3, scheduler: DispatchQueue.main)
.retry(3)
그러나 .delay
는 오류 발생 여부에 관계없이 실행되기에 이상적인 해결책은 아님니다. 저희가 하고 싶은 것은 오류가 발생한 경우에만 .delay
를 하는것임니다. 이는 위에서 본 .catch
를 사용하면 가능함니다.
URLSession.shared.dataTaskPublisher(for: url)
.catch { _ in
URLSession.shared.dataTaskPublisher(for: url)
.delay(for: 3, scheduler: DispatchQueue.main)
}.retry(3)
하지만 위 코드는 .retry
횟수를 위반함니다. 연결했다가 실패하고, 새 데이터 게시자로 대체하고 다시 시작하고 등등은 연결을 여러번 할 가능성이 있습니다. 이 문제는 .share
를 사용하여 초기 퍼블리셔에 대한 참조를 만들어서 해결할 수 있습니다.
let pub = URLSession.shared.dataTaskPublisher(for: url).share()
let head = pub.catch { _ in
pub.delay(for: 3, scheduler: DispatchQueue.main)
}.retry(3)
위 예시코드처럼 변수로 만들어서 참조를 넘기면 catch
가 실행되어 .delay
가 적용된 동일 데이터 작업 게시자를 반화합니다.
codables
란 Swift의 코더블 프로토콜을 처리하는 연산자들을 의미함니다. .encode, .decode
두 가지가 존재함니다.
encode(Publishers.Encode): JSONEncoder or PropertyListEncoder
를 받아서 업스트림에서 오는 값을 인코딩 함니다. 업스트림 출력 타입은 Encodable
을 준수해야함니다. 다운스트림 타입은 Data
임니다. 인코딩에 실패하면 실패가 파이프라인에 전달됨니다.
.decode(Publishers.Decode): 타입(Decodable을 준수하는)과 JSONDecoder or PropertyListDecoder
를 받아 업스트림에서 오는 값을 디코딩함니다. 업스트림 출력 타입은 Data
여야 함니다. 다운스트림 타입은 첫 번째 매개변수로 전달한 타입이 됨니다. 디코딩에 실패하면 실패가 파이프라인에 전송됨니다.
인코딩 또는 디코딩이 실패하면 취소 호출이 업스트림에 전송되고 전체 파이프라인이 종료되기 때문에 이러한 상황을 원치않는다면, 해결방법으로 .flatMap
을 사용하여 실패를 내부 파이프라인으로 제한하는 방법이 있슴니다.
flatMap 내부에서 퍼블리셔를 생성하고, replaceError, catch 등으로 에러를 내부로 제한시키면 전체 파이프라인은 취소되지 않슴니다!
threaders
란 메시지를 전송할 스레드(스케줄러)를 지정하는 연산자를 의미합니다.
보통 dataTaskPublisher
로 작업하는 경우 거의 항상 파이프라인 어딘가에 .receive(DispatchQueue.main)
을 포함합니다. 퍼블리셔의 출력이 메인스레드에 도착한다고 보장할 수 없기 때문임니다. 해당 출력으로 .assign
으로 저장하는 등의 작업을 수행하면 메인스레드에서 해당 작업을 수행해야 할 것임니다.
두 메서드 모두 options:
가 있지만 거의 상용할 가능성은 없다고하네용