ErrorHandling
옵저버블에서 에러가 발생하면 에러이벤트가 발생하고 구독자는 더이상 새로운 이벤트를 받지 못한다.
예를들어 옵저버블이 네트워크요청을 처리하고 구독자가 UI를 업데이트하는 패턴를 생각해본다.
보통 UI를 업데이트 하는 코드는 NextEvent에서 처리하는데 EE가 전달되게되면 구독이 종료되고 더이상 NE가 전달되지 않는다.
그래서 UI를 업데이트하는 코드는 실행되지 않는다.
RxSwift는 두 가지 방법으로 이런 문제를 해결한다.
첫 번째로 에러이벤트가 전달되면 새로운 옵저버블을 리턴한다.
여기에선 Catch연산자를 사용한다.
Catch연산자는 NE와 CE는 그대로 구독자에게 전달한다. 반면 옵저버블에서 옵저버블에서 에러이벤트를 방출하면 새로운 옵저버블로 바꿔서 구독자에게 전달한다
다시네트워크 요청을 생각해본다.
서버에 접속할 수 없거나 URL이 잘못되었다면 구독자에게 EE가 전달된다.
EE가 그대로 전달되면 UI가 더이상 업데이트되지 않아서 앱을 더이상 사용할 수 없게된다.
catch연산자를 사용해서 옵저버블로 바꿔서 전달하고 옵저버블에서 기본값이나 로컬캐시를 방출하도록 하면 이런 문제가 해결된다.
두 번째 방법에선 에러이벤트가 발생하면 옵저버블을 다시 구독한다
이때는 retry연산자를 사용한다.
에러가 발생하지 않을 때까지 무한정 재시도하거나 재시도 횟수를 제한하는 방법을 제공한다.
Catch(옵저버블 리턴)
Catch연산자는 NE와 CE는 그대로 방출한다. 반면 EE는 방출하지 않고 새로운 옵저버블이나 기본값을 방출한다.
Catch는 여러 상황에서 사용하지만 네트워크 요청에서 많이 사용된다
요청이 실패했을 때 로컬캐시를 사용하거나 아니면 기본값을 사용하도록 구현한다.
public func `catch`(_ handler: @escaping (Swift.Error) throws -> Observable<Element>)
-> Observable<Element> {
Catch(source: self.asObservable(), handler: handler)
}
catch 는 클로저를 파라미터로 받는다. EE는 클로저에 파라미터로 전달되고 클로저는 새로운 옵저버블을 리턴한다. 그리고 이옵저버블은 소스옵저버블과 동일한 값을 방출한다.
catch연산자는 소스 옵저버블이 EE를 방출하면 소스옵저버블을 클로저가 방출하는 옵저버블로 방출한다.
소스옵저버블은 다른이벤트를 전달하지 못하지만 교체된 옵저버블은 문제가 없기 때문에 다른 이벤트를 계속 방출할 수 있다.
let subject = PublishSubject<Int>()
let recovery = PublishSubject<Int>()
subject
.subscribe { print($0) }
.disposed(by: bag)
subject.onError(MyError.error)
//error(error)
서브젝트에 EE를 보내면 지금은 구독자에게 그대로 EE가 전해진다. 그리고 바로 구독이 종료되기 때문에 더이상 새로운 NE를 받을 수 없다.
여기에 catch연산자를 추가한다. 클로저에서 recovery를 return한다.
let subject = PublishSubject<Int>()
let recovery = PublishSubject<Int>()
subject
.catch{ _ in recovery}
.subscribe { print($0) }
.disposed(by: bag)
subject.onError(MyError.error)
subject.onNext(123)
이번에는 EE가 전달되지 않았다 catch연산자가 원본서브젝트를 recovery 서브젝트로 교체했기 때문이다.
subject는 더이상 다른 이벤트를 전달하지 못한다. 그래서 이 값(123)은 구독자에게 전달되지 않는다.
반면 recovery서브젝트는 아무런 문제가 없기 때문에 전달할 수 있다.
let subject = PublishSubject<Int>()
let recovery = PublishSubject<Int>()
subject
.catch{ _ in recovery}
.subscribe { print($0) }
.disposed(by: bag)
subject.onError(MyError.error)
subject.onNext(123)
recovery.onNext(11)
recovery.onCompleted()
//next(22)
//completed
recovery에 전달하면 구독이 교체되기 때문에 새로운 NE를 전달할 수 있고 CE를 방출하면 정상적으로 구독이 종료된다.
catch 연산자는 소스옵저버블이 방출한 에러를 새로운 옵저버블로 교체하는 방식으로 처리한다.
catchAndReturn(옵저버블 대신 기본값을 리턴)
이 연산자는 이름이 모든것을 설명한다. 소스 옵저버블에서 에러이벤트가 발생하면 파라미터로 전달한 기본값을 방출한다.
파라미터의 형식은 항상 소스옵저버블이 방출하는 형식과 같다.
let subject = PublishSubject<Int>()
subject
.catchAndReturn(-1)
.subscribe { print($0) }
.disposed(by: bag)
subject.onError(MyError.error)
//next(-1)
//completed
EE가 발생하게 되면 catchAndReturn에 전달된 파라미터값이 구독자에게 전달된다.
소스옵저버블은 더이상 값을 전달할 수 없고 파라미터로 전달한 값은 옵저버블이아닌 하나의 값이다.
더이상 전달할 이벤트가 없기 때문에 바로 CE를 전달하고 모든 시퀀스를 끝낸다.
retry
소스옵저버블이 에러이벤트를 방출하면 구독을 해제하고 새로운 구독을 시작한다.
새로운 구독이 시작되기 때문에 옵저버블의 모든작업은 처음부터 다시 시작된다.
옵저버블에서 에러가 발생하지 않으면 정상적으로 종료되고,
에러가 발생하면 또다시 새로운 구독을 시작한다.
public func retry() -> Observable<Element> {
CatchSequence(sources: InfiniteSequence(repeatedValue: self.asObservable()))
}
public func retry(_ maxAttemptCount: Int)
-> Observable<Element> {
CatchSequence(sources: Swift.repeatElement(self.asObservable(), count: maxAttemptCount))
}
retry 연산자는 두가지 형태가 있다. 첫 번째처럼 파라미터 없이 전달한다면 옵저버블이 정상적으로 종료될때 까지 계속해서 재시도한다.
만약 옵저버블이 반복적으로 EE가 발생한다면 재 시도 횟수가 늘어나고 그만큼 리소스를 낭비한다.
심한경우엔 무한Loop처럼 touch이벤트를 처리할 수 없거나, 앱이 강제로 종료되는 문제가 발생한다.
그래서 파라미터가 없는 연산자는 조심해서 사용해야한다.
아래있는 연산자를보면 재시도횟수를 파라미터로 받는다. 그래서 위에 설명한 문제가 발생하지 않는다.
재시도 횟수를 전달할 때는 항상 1을 더해서 전달해야한다.
var attempts = 1
let source = Observable<Int>.create { observer in
let currentAttempts = attempts
print("#\(currentAttempts) START")
if attempts < 3 {
observer.onError(MyError.error)
attempts += 1
}
observer.onNext(1)
observer.onNext(2)
observer.onCompleted()
return Disposables.create {
print("#\(currentAttempts) END")
}
}
source
.subscribe { print($0) }
.disposed(by: bag)
//#1 START
//error(error)
//#1 END
attempts에 저장된 값이 3보다 작다면 EE를 방출하고 변수를 1씩 증가시킨다.
그리고 시퀀스의 시작과 끝을 알 수 있도록 로그를 추가해놨다.
지금은 첫 번째 실행에서 바로 EE가 전달되고 구독이 종료된다.
여기에 retry연산자를 추가한다.
var attempts = 1
let source = Observable<Int>.create { observer in
let currentAttempts = attempts
print("#\(currentAttempts) START")
if attempts < 3 {
observer.onError(MyError.error)
attempts += 1
}
observer.onNext(1)
observer.onNext(2)
observer.onCompleted()
return Disposables.create {
print("#\(currentAttempts) END")
}
}
source
.retry()
.subscribe { print($0) }
.disposed(by: bag)
/*
#1 START
#1 END
#2 START
#2 END
#3 START
next(1)
next(2)
completed
#3 END
*/
처음 두번의 시도는 실패하고 세 번째 시도에서 성공했다.
이렇게 결과가 나오는 이유는 여기에서 attempts값이 3보다 작은 경우에만 EE를 방출하고 있기 때문이다.
만약 조건을 attempts > 0이렇게 바꾼다면 계속 EE만 방출할것이다. 이렇게 되면 무한루프가 발생하기 때문에 재시도 횟수를 지정하는 방식으로 구현해야한다.
마지막 이벤트까지 실패라면 EE를 방출하고 종료된다.
여기서 재시도 횟수는 원하는 만큼의 +1이어야한다
만약 6번 재시도하길 원한다면 .retry(7)을 전달해야한다
또한 재시도 횟수 이내에 작업이 정상적으로 종료되었다면 재시도하지 않고 작업이 종료된다.
retry연산자는 에러가 발생한 즉시 재시도하기 때문에 우리가 재시도하는 시점을 제어하긴 불가능하다.
네트워크 요청에서 에러가 발생했다면 정상적인 응답을 받거나 최대 횟수에 도달할 때 까지 계속 재시도한다.
만약 사용자가 재시도 버튼을 탭하는 시점에만 재시도하도록 구현하고 싶다면?
이때는 retryWhen 연산자를 사용한다.
retryWhen
만약 사용자가 재시도 버튼을 탭하는 시점에만 재시도하도록 구현하고 싶다면?
이때는 retryWhen 연산자를 사용한다.
public func retry<TriggerObservable: ObservableType, Error: Swift.Error>(when notificationHandler: @escaping (Observable<Error>) -> TriggerObservable)
-> Observable<Element> {
RetryWhenSequence(sources: InfiniteSequence(repeatedValue: self.asObservable()), notificationHandler: notificationHandler)
}
이 연산자는 클로저를 파라미터로 받는다. 클로저 파라미터에는 발생한 에러를 방출하는 옵저버블이 전달된다.
그리고 클로저는 트리거 옵저버블을 리턴한다. 트리거 옵저버블이 넥스트이벤트를 방출하는 시점에 소스옵저버블에서 새로운 구독을 시작한다. 다시말해 작업을 재시도한다.
var attempts = 1
let source = Observable<Int>.create { observer in
let currentAttempts = attempts
print("START #\(currentAttempts)")
if attempts < 3 {
observer.onError(MyError.error)
attempts += 1
}
observer.onNext(1)
observer.onNext(2)
observer.onCompleted()
return Disposables.create {
print("END #\(currentAttempts)")
}
}
let trigger = PublishSubject<Void>()
source
.retry{ _ in trigger }
.subscribe { print($0) }
.disposed(by: bag)
//START #1
//END #1
첫 번째 실행에 바로 에러가 발생한다. 그리고 바로 재시도하지 않는다.
하지만 트리거가 NE를 방출할때까지 대기한다.
source
.retry{ _ in trigger }
.subscribe { print($0) }
.disposed(by: bag)
trigger.onNext(())
//START #1
//END #1
//START #2
//END #2
이렇게 트리거에서 NE가 방출된다면 재시도하고, 다시 에러이벤트가 발생하면 다시 trigger에서 NE가 방출될 때 까지 대기한다.
source
.retry{ _ in trigger }
.subscribe { print($0) }
.disposed(by: bag)
trigger.onNext(())
trigger.onNext(())
/*
START #1
END #1
START #2
END #2
START #3
next(1)
next(2)
completed
END #3
*/
두 번째 재시도에선 변수의 값이 3 이상이기 때문에 더이상 재시도를 대기하지않고 CE를 방출했기 때문에 정상 종료한다.