[Combine] Operators - 05

rbw·2023년 12월 23일
0

Combine

목록 보기
9/11

Error Handlers

얘는 .map, filter 와 같은 연산자가 값에 대해 수행하는 작업을 오류(즉, 실패)에 대해 수행하는 연산자를 의미합니다. 값과 오류는 모두 파이프라인을 따라 내려오는 객체이며 각각 타입이 있습니다.

한 값을 다른 값으로, 심지어 다른 타입으로 변환하거나 값이 파이프라인에서 이동 못하도록 할 수 있는 것처럼 오류에 대해서도 같은 종류의 작업을 할 수 있습니다.

오류 핸들러 중 가장 뛰어난 것은 .mapError라고 하네요. .map이 하는 일을 똑같이 수행하지만 오류에 대해서는 업스트림에서 오류를 수신하면 다른 오류 개체, 심지어 다른 타입의 오류 객체를 포함하는 오류를 다운스트림으로 보낼 수 있습니다.

Map Error

.mapError (Publishers.MapError)는 입력이 Error인 맵함수를 받습니다. 일반적으로 이 연산자는 아무 작업도 하지 않고 위에서 받은 값을 전달할 뿐입니다. .finished 를 받으면 해당 값도 함께 전달함니다. 그러나 .failure를 받으면 실패에 포함된 오류를 가져와서 전달합니다. 맵 함수는 오류를 반환해야 하며, 이 연산자는 오류를 새 .failure로 감싸서 파이프라인으로 전달함니다.

얘는 다운스트림으로 전달되는 오류의 세부사항을 변경할 수 있을 뿐만 아니라 오류 타입 자체를 변경할 수도 있습니다 예를 들어 URLError.cancelled를 수신한 경우 얘는 URLError.cannotConnectToHost로 대체하거나, 더 나아가 아예 다른 에러타입으로도 대체가능함니다. 따라서 이 연산자는 파이프라인의 다운스트림 부분과 관련된 Failure타입을 변경할 수 있습니다.

.tryMap 은 오류가 Error를 만족하는 것만 알고 있기 때문에 다운스트림 부분에서 실패 타입을 더 정확하게 보려면 .mapError를 사용할 수 있습니다.

.tryMap {
		 ...
}
.mapError { $0 as! URLError }

참고로 업스트림 실패 타입이 Never인 경우는 .mapError를 사용할 수 없습니다. 대신 .setFailureType 을 사용하시믄댐니다.

ReplaceError

.replaceError(Publishers.ReplaceError 는 다운스트림 파이프라인으로 전달되는 오류 타입을 업스트림의 오류 타입이 무엇이든 간에 Never로 변경함니다. 이는 업스트림에서 오류가 발생할 경우 값으로 출력될 업스트림의 출력 타입의 값을 지정하여 수행됨니다.

URLSession.shared.dataTaskPublisher(for: url)
	.map {$0.data} 
	.replaceError(with: Data())

.map 을 통과할 때까지 파이프라인의 출력 타입은 Data이고 실패 타입은 URLError임니다. 이제 위 연산자를 사용해서 위에서 오류가 발생하면 이를 차단하고 대신 자체 Data()를 반환해야 한다고 말함니다. 이렇게 하면 다운스트림에 오류가 발생하지 않음을 보장할 수 있으므로 오류 타입은 Never가 됨니다.

SetFailureType

.setFailureType (Publishers.SetFailureType) 은 업스트림 오류 타입이 Never인 경우 다운스트림 파이프라인의 오류 타입을 일종의 Error로 변경하는 친구임니다.

try로 시작하는 연산자를 사용하는 경우에는 이 친구를 굳이 사용할 필요는 없습니다. 위에 실패 유혀이 Never 인 경우에 주로 사용하고, 아래 다운스트림의 에러 타입을 맞추기 위해서도 주로 사용합니다.

self.myButton.publisher()
	.setFailureType(to: Error.self)
	.flatMap { _ in checkAccess().publisher }

Catch

.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

.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

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

threaders란 메시지를 전송할 스레드(스케줄러)를 지정하는 연산자를 의미합니다.

  • receive(on:) (Publishers.ReceiveOn): 보통 얘를 많이 사용함니다. 기본적으로 파이프라인의 다운스트림 부분이 지정된 스레드에서 값과 기타 메시지를 수신하도록 보장됨니다.

보통 dataTaskPublisher로 작업하는 경우 거의 항상 파이프라인 어딘가에 .receive(DispatchQueue.main)을 포함합니다. 퍼블리셔의 출력이 메인스레드에 도착한다고 보장할 수 없기 때문임니다. 해당 출력으로 .assign으로 저장하는 등의 작업을 수행하면 메인스레드에서 해당 작업을 수행해야 할 것임니다.

  • subscribe(on:) (Publishers.SubscribeOn): 이 친구는 업스트림 방향으로 작동함니다. 즉 구독, 값 요청, 취소 메시지에 영향을 미침니다.

두 메서드 모두 options: 가 있지만 거의 상용할 가능성은 없다고하네용

profile
hi there 👋

0개의 댓글