본문 바로가기
RxSwift

RxSwift에 관하여(Error Handling Operators)

by iOS 개린이 2023. 8. 8.

Error Handling Operators

말 그대로 에러가 발생할 때, 이를 처리하는 방법을 정의하는 연산자이다. 

Catch와 Retry 에 대해서 학습해보자.

 

 

1. Catch

 

Observable에서 에러가 발생했을 경우, 그 에러를 처리하고 새로운 요소 또는 시퀸스를 반환함으로써 시퀸스가 정상적으로 종료되도록 하거나 종료되지 않도록 할 수 있다. 이를 통해 에러가 발생하더라도 프로그램이 중단되지 않고 적절한 복구 루틴을 수행할 수 있게 해준다.

 

 

catch의 사용예시

let observableWithError: Observable<Int> = Observable.create { observer in
    observer.onNext(1)
    observer.onNext(2)
    observer.onError(MyError.someError) // 에러 이벤트 발생
    return Disposables.create()
}

observableWithError
    .catchError { error in
        return Observable.just(-1) // 에러가 발생하면 -1을 반환하는 새로운 Observable 생성
    }
    .subscribe(onNext: { value in
        print(value)
    })
    .disposed(by: disposeBag)

//1
//2
//-1

 

원본 Observable에서 .onError 이벤트를 방출했을 경우, 'catchError' 가 이를 캐치하여 새로운 Observable을 생성하고 반환한다. 

 

 

 

catch의 정의

catch 역시 'ObservableType' 프로토콜의 extension 메서드에 정의되어 있다.

또한 catch 는 2가지 형태로 정의되어 있다. 하나씩 살펴보자.

 

1. 사용예시에서 사용한 catchError 

public func `catch`(_ handler: @escaping (Swift.Error) throws -> Observable<Element>)
        -> Observable<Element> {
        Catch(source: self.asObservable(), handler: handler)
    }

 

매개변수 'handler' 는 Swift의 'Error' 타입을 받아서, 에러를 처리한 후 새로운 Observable 시퀸스를 반환하는 클로저이다.

 

반환 타입은 Observable<Element> 로 원본 Observable을 가지는 'Catch' Observable을 반환한다.

원본에서 에러 이벤트를 방출하면, 원본을 dispose하고, handler를 통해 에러를 처리하고 반환되는 새로운 Observable을 구독하는 작업을 한다. 반면, 에러가 방출되지 않으면 원본에서 전달하는 이벤트를 그대로 전달하는 것으로 보인다! (코드를 뜯어본 결과)

 

 

2. catchAndReturn

public func catchAndReturn(_ element: Element) -> Observable<Element> {
     Catch(source: self.asObservable(), handler: { _ in Observable.just(element) })
}

 

원본 Observable에서 에러 이벤트를 방출했을 경우, 이를 캐치하고 지정한 'element' 를 반환하는 메서드이다.

매개변수 'element' 는 에러가 발생할 경우, 반환될 단일 요소이다.

 

위 'catchError' 형태와 동일하게 함수 내부에서는 Catch 객체가 '원본 Observable' 과 'handler' 를 전달받고 있는데, 

여기서 'handler' 는 하나의 요소만을 방출하는 'just' Observable이 반환되도록 정의하고 있다.

 

catchAndReturn의 사용예시

let numbersWithError: Observable<Int> = Observable.create { observer in
    observer.onNext(1)
    observer.onNext(2)
    observer.onError(MyError.someError)
    return Disposables.create()
}

numbersWithError
    .catchAndReturn(0)
    .subscribe(onNext: { value in
        print(value) 
    })

//1
//2
//0

 

에러가 발생하면 'catchAndReturn' 가 에러를 캐치하여 0을 반환하는 것을 볼 수 있다.

 

 

 

 

2. Retry

 

말 그대로 Observable이 에러를 발생시킬 경우, 해당 Observable에 다시 구독(재시도)을 해주는 역할을 수행한다.

Observable의 이벤트 방출이 일시적인 에러로 인해 중단되는 상황에서 재시도를 통해 문제를 해결할 수 있는 경우 retry를 사용한다.

 

 

Retry의 사용예시

var attemptCount = 0
let maxAttempts = 3

let failingRequest = Observable<String>.create { observer -> Disposable in
    attemptCount += 1
    print("시도: \(attemptCount)")
    
    if attemptCount < maxAttempts {
        observer.onError(MyError.someError) // 일시적인 실패
    } else {
        observer.onNext("성공!") // 성공
        observer.onCompleted()
    }
    
    return Disposables.create()
}

failingRequest
    .retry(maxAttempts) // 최대 maxAttempts회 재시도
    .subscribe(onNext: { value in
        print(value)
    }, onError: { error in
        print("Error occurred: \(error)")
    })


//시도: 1
//시도: 2
//시도: 3
//성공!

 

 

원본 Observable에서 2번까지만 일시적인 에러가 발생하도록 설정해두고,

retry 연산자를 이용하여 3번의 재구독 끝에 작업을 성공시키는 것을 볼 수 있다. 

 

 

retry의 정의

retry 는 'ObservableType' 프로토콜의 extension 메서드에 정의되어 있다.

retry 또한 2가지 형태로 정의되어 있다. 하나씩 살펴보자.

 

1. 사용예시에서 사용한 retry 

public func retry(_ maxAttemptCount: Int)
        -> Observable<Element> {
        CatchSequence(sources: Swift.repeatElement(self.asObservable(), count: maxAttemptCount))
    }

 

이 retry 연산자는 에러가 발생했을 경우, 지정된 'maxAttempCount' 만큼 원본 Observable 시퀸스를 반복한다.

만약 특정 횟수 내에서 성공적으로 종료되지 않으면, 연산자는 에러를 발생시키고 시퀸스를 종료한다.

 

메서드 내부에서는 'CatchSequence' 객체의 인스턴스를 생성하고 반환한다.

'CatchSequence' 의 매개변수 'sources' 에 전달되는 'Swift.repeatElement' 는 Swift 라이브러리에 정의되어 있는 함수로, 지정된 요소(self.asObservable)를  특정 횟수(count)만큼 반복하여 생성하는 시퀸스를 반환한다.

 

따라서 현재 Observable 인스턴스를 maxAttemptCount 만큼 반복한 시퀸스를 생성하여 전달한다.

예를 들어 count에 2가 할당되었다면, [asObservable, asObservable] 시퀸스가 전달되는 것임. 

 

 

2. 반복 횟수가 없는 retry

public func retry() -> Observable<Element> {
        CatchSequence(sources: InfiniteSequence(repeatedValue: self.asObservable()))
    }

 

이 retry 연산자는 원본 Observable의 시퀸스가 성공적으로 종료될 때까지 반복한다. 

 

'CatchSequence' 의 매개변수 'sources' 를 보면, 'InfiniteSequence' 를 전달하고 있다.

'InfiniteSequence' 는 이름에서도 유추할 수 있듯이 현재 Observable 인스턴스(self.asObservable) 를 무한하게 반복하는 시퀸스를 생성하는 것이다. 따라서 원본 Observable에서 에러가 발생할 때마다 무한하게 다음 시퀸스로 넘어가는 작업을 수행한다.

 

코드를 뜯어보니 'CatchSequence' 의 내부에서는 전달받은 sourecs(옵저버블 시퀸스)를 저장하고, 'CatchSequenceSink' 를 생성하여 원본 Observable에서 방출되는 이벤트를 처리한다.

 

.next, .completed 이벤트에 대해서는 그대로 옵저버에게 전달하고 있고,

에러가 발생하면 스케줄러에게 다음 시퀸스 옵저버블의 이벤트를 받을 수 있도록 하는 작업을 수행하는 것으로 보인다.

 

 

 

retry 사용시 주의할 점이나 한계점

1. 무한 루프

매개변수가 없는 retry 즉, 무한 시퀸스를 생성하는 retry 연산자를 사용할 때는 원본 Observable이 계속해서 에러를 발생시키면 무한 루프에 빠질 수 있기 때문에 꼭 필요한 상황에서 사용하는 것이 좋다. (웬만하면 횟수를 지정하는 것이 좋을 것 같네요)

 

2. 중복 방출

retry 연산자는 에러가 방출되기 전까지의 모든 항목을 중복해서 방출할 수 있다.

1, 2, 3, 4, 5 의 정수를 방출하는 도중 1, 2 까지만 항목을 방출하고 에러가 발생했다면, 

retry는 다시 처음부터 항목을 방출하기 때문에 중복된 항목이 방출될 수 있다.

 

3. 지연 시간

일시적인 에러가 발생한다고 했을 경우, retry의 재시도 주기가 너무 빠르다면 계속해서 에러를 발생시킬 수 있기 때문에,

어느정도 지연 시간을 설정하고, retry를 시도하는 것도 좋은 방법이다!

 

 

 

 

 

 

Reference

-https://reactivex.io/documentation/operators/retry.html