본문 바로가기
Rx

RxJava 디버깅과 예외처리 -4 (retry, retryUntil, retryWhen 함수)

by 봄석 2018. 12. 31.

본 내용은 필자가 학습한 내용을 정리하는 내용입니다.

대부분 의 내용이 아래 책의 내용이므로 원서를 구매해서 직접보시는걸 추천드립니다!

RxJava 프로그래밍 리액티브 프로그래밍 기초부터 RxAndroid까지 한 번에

유동환 , 박정준 지음 | 한빛미디어 | 2017년 09월 04일 출간

http://www.kyobobook.co.kr/product/detailViewKor.laf?ejkGb=KOR&mallGb=KOR&barcode=9788968488658&orderClick=LAV&Kc=


저자님의 블로그

https://brunch.co.kr/@yudong#info




retry() 함수

예외 처리의 다른 방법은 재시도 입니다. 예를 들어 서버와 통신할 때 인터넷이 일시적으로 안되거나 서버에 일시적인 장애가 발생하면 클라이언트에서는 일정 시간 후에 다시 통신을 요청하는 것이 필요합니다. 이때 1개의 API가 아닌 API 다수를 연속해서 호출해야 하는 경우 재시도하는 시나리오가 매우 복잡해질 수 있기도 합니다.


RxJava는 이러한 것을 단순하게 처리할 수 있는 retry() 라는 함수를 제공합니다.

onError이벤트 발생 시 해당 처리를 재시도 합니다.


retry() 마블 다이어그램

출처 - http://reactivex.io/RxJava/javadoc/io/reactivex/Observable.html


retry() 함수는 Observable에서 onError 이벤트가 발생하면 바로 다시 subscribe() 함수를 재호출하여 재구독하게 되어있습니다.


retry() 함수의 원형은 아래와 같습니다 .기본 구현 내용에서 무한대로 재시도 하도록 설정했으므로 실무에서 활용할 수 있는 다양한 오버로딩이 제공됩니다. 인자는 재시도 횟수를 지정하거나 어떤 조건에서도 재시도(재구독) 할 것인지를 판단합니다.


retry() 함수의 원형

public final Observable<T> retry()
public final Observable<T> retry(
    BiPredicate<? super Integer, ? supter Throwable> predicate)
public final Observable<T> retry(long times)
public final Observable<T> retry(long times, Predicate<? supter Throwable> predicate)

public final Observable<T> retry(Predicate<? super Throwable> predicate)




아래 예제는 위 함수의 원형 중 굵은 글씨로 표기한 두 오버로딩 함수를 이용해 GitHub API를 호출하는 예제입니다.


retry() 함수의 활용 예

//시간표시용
CommonUtils.exampleStart();
        
String url="https://api.github.com/zen";
Observable<String> source=Observable.just(url)
            .map(OkHttpHelper::getT)
            .retry(5)
            .onErrorReturn(e->CommonUtils.ERROR_CODE);
        

 source.subscribe(data->Log.it("result : "+data));


\

실행 시간을 표시하기 위해 CommonUtils.exampleStart()를 호출한 후 url에 정해진 URL을 저장합니다. 그리고 map(urlOkHttpHelper::getT)를 호출한 후 retry() 함수의 실행 횟수는 5회로 지정합니다.

마지막으로 에러 발생시 ERROR_CODE를 리턴합니다. 참고로 OkHtttpHelper.getT()는 기존의 OkHttpHelper.get()과 동일하지만 시간표시를 위한 로그만 변경한 것입니다.


실행결과는 아래와 같습니다 . 단, 재시도 동작을 확인하기 위해 인터넷을 끄거나 비행기 모드를 켜고 실행해야 합니다.

main | 1652 | error = api.github.com
main | 1653 | error = api.github.com
main | 1653 | error = api.github.com
main | 1654 | error = api.github.com
main | 1654 | error = api.github.com
main | 1654 | error = api.github.com

main | 1654 | value = result : -500



총 5회의 재시도 후 최종 요청이 실패되었습니다.

그런데 위의 실행시간을 보면 문제가 있습니다. 첫 재시도 시간(1653)까지 1ms가  걸렷고, 이후에도 1ms가 되지 않는 시간에 바로 재시도 하고있습니다. 이러면 대기 시간이 없어 실제로 도움이 되지 않습니다.


대기시간을 설정한 retry() 함수 예

final int RETRY_MAX=5;
        final int RETRY_DELAY=1000;
        //시간표시용
        CommonUtils.exampleStart();
        
        String url="https://api.github.com/zen";
        Observable<String> source=Observable.just(url)
                .map(OkHttpHelper::getT)
                .retry((retryCnt,e)->{
                    Log.e("retryCnt:"+retryCnt);
                    CommonUtils.sleep(RETRY_DELAY);
                    
                    return retryCnt< RETRY_MAX ? true :false;
                })
                .onErrorReturn(e->CommonUtils.ERROR_CODE);
        

        source.subscribe(data->Log.it("result : "+data));


재시도 횟수는 5회로 설정, 재시도 간격은 1000ms로 설정했습니다.

retry() 함수는 람다 표현식의 인자로 retryCnt와 Throwable 객체를 전달 받습니다. 재시도 할 때는 COmmonUtils.sleep() 함수를 호출하여 1000ms 동안 대기합니다.


마지막으로 재시도 횟수를 제한하기 위해 재시도 횟수가 5회 이내일대는 람다표현식이 true를 리턴하고 5회 이후에는 false를 리턴합니다.


실행결과는 아래와 같습니다. 만약 인터넷이 연결된 상태라면 정상적으로 GitHub API 의 호출이 이루어지므로 재시도하지않습니다.

main | 1469 | error = api.github.com
main | error = retryCnt:1
main | 2471 | error = api.github.com
main | error = retryCnt:2
main | 3472 | error = api.github.com
main | error = retryCnt:3
main | 4473 | error = api.github.com
main | error = retryCnt:4
main | 5475 | error = api.github.com
main | error = retryCnt:5

main | 6476 | value = result : -500





retryUntil() 함수

retryUntil() 함수는 특정 조건이 충족될 때 까지만 재시도하는 함수입니다. retry() 함수는 재시도를 지속할 조건이 없을 때 재시도를 중단한다면, retryUntil() 함수는 재시도를 중단할 조건이 발생할 때까지 계속 재시도 합니다.


retryUntil() 함수의 원형

public final Observable<T> retryUntil(final BooleanSupplier stop)


BooleanSupplier 객체는 인자도 없고 Boolean 값을 리턴하는 함수형 인터페이스 입니다.
public interface BooleanSupplier{
    boolean getAsBoolean() throws Exception;
}



retryUntil() 함수의 활용 예

CommonUtils.exampleStart();
        String url="https://api.github.com/zen";
        Observable<String> source=Observable.just(url)
                .map(OkHttpHelper::getT)
                .subscribeOn(Schedulers.io())
                .retryUntil(()->{
                    if(CommonUtils.isNetworkAvailable())
                        return true; //중지
                    CommonUtils.sleep(1000);
                    return false; //계속실행
                });
        source.subscribe(Log::i);
        
        //IO 스케줄러에서 실행되기 떄문에 sleep() 함수가 필요함

        CommonUtils.sleep(5000);


보통 재시도 로직은 별도의 스레드에서 동작하기 때문에 IO 스케줄러를 활용합니다. 따라서 

subscribeOn() 함수안에는 Schedulers.io() 인자를 넣었습니다.


retryUntil() 함수의 인자인 람다 표현식에는 먼저 CommonUtils.isNetworkAvailable()를 호출하여 네트워크가 사용 가능한 상태인지 확인합니다. 만약 true 를 리턴하면 재시도를 중단하도록 람다 표현식도 true를 리턴합니다. 네트워크를 사용할 수 없는 상태라면 1000ms를 쉬고 재시도(재구독)합니다. 이때 람다 표현식은 false를 리턴합니다.


CmmonUtils.isNetworkAvailable() 함수

public static boolean isNetworkAvailable() { 
        try {
            return InetAddress.getByName("www.google.com").isReachable(1000);
        } catch (IOException e) {
            Log.v("Network is not available");    
        }
        return false;
    }



편의상 유명한 사이트인 'www.google.com'에 접속할 수 있는지 확인하여 인터넷에 접속할 수 있는지 간접적으로 확인하도록 구현되어 있습니다 .최대 대기시간(timeout)은 1000ms 입니다 .


실행결과는 아래와 같습니다.

RxCachedThreadScheduler-1 | 1486 | error = api.github.com
RxCachedThreadScheduler-1 | Network is not available
RxCachedThreadScheduler-1 | 2489 | error = api.github.com
RxCachedThreadScheduler-1 | Network is not available
RxCachedThreadScheduler-1 | 3490 | error = api.github.com
RxCachedThreadScheduler-1 | Network is not available
RxCachedThreadScheduler-1 | 4494 | error = api.github.com

RxCachedThreadScheduler-1 | Network is not available





retryWhen() 함수

재시도와 관련있는 가장 복잡한 함수입니다. javadoc을 보아도 함수의 원형을 이해하기가 어렵습니다 . retryWhen() 함수의 람다 표현식 인자는 Observable<Throwable>입니다.


retryWhen() 함수의 마블 다이어그램

출처 - http://reactivex.io/RxJava/javadoc/io/reactivex/Observable.html



아래의 코드는 세번의 재시도를 하며, 재시도 횟수가 늘어날때마다 1000ms 씩 재시도 시간이 늘어납니다. 


retryWhen() 함수의 활용 예

Observable.create((ObservableEmitter<String> emitter) -> {
              System.out.println("subscribing");
              emitter.onError(new RuntimeException("always fails"));
          }).retryWhen(attempts -> {
              return attempts.zipWith(Observable.range(1, 3), (n, i) -> i).flatMap(i -> {
                  System.out.println("delay retry by " + i + " second(s)");
                  return Observable.timer(i, TimeUnit.SECONDS);
              });

          }).blockingForEach(System.out::println);


먼저 Observable은 데이터 발행을 항상 실패하도록 설정했습니다. retryWhen() 함수의 인자인 attempts는 Observable입니다. 이 상태에서 재시도할 때는 Observable.range(1,3)과 zip() 함수로 두 Observable을 합성합니다. 즉 ,3번 재시도 한다는 뜨십니다. 또 한 재시도할 때 매번 timer() 함수를 호출하여 1000ms씩 대기합니다. 


실행결과는 아래와 같습니다

subscribing
delay retry by 1 second(s)
subscribing
delay retry by 2 second(s)
subscribing
delay retry by 3 second(s)

subscribing


굵은 글씨로 표시된 부분처럼 세번 재시도 했고 재시도 횟수에 따라 1000ms( 1초) 씩 대기시간이 늘어납니다.






댓글