예외처리
RxJava 프로그래밍에서 예외처리는 어떻게 할까요 ? 지금까지 onError 이벤트를 전달 받아서 예외처리를 한다고 했었습니다. 원래 자바에서는 정상적인 실행 코드와 예외처리 코드를 분리하기 위해서 try-catch 문을 제공합니다 . try-catch 문으로 예외처리를 한다면 아래코드처럼 작성하게 됩니다.
Observable<String> source = Observable.create(
(ObservableEmitter<String> emitter) -> {
emitter.onNext("1");
emitter.onError(new Exception("Some Error"));
emitter.onNext("3");
emitter.onComplete();
});
try {
source.subscribe(Log::i);
} catch (Exception e) {
Log.e(e.getMessage());
}
하지만 코드를 실행해보면 다음과 같은 결과가 나옵니다
main | value = 1
io.reactivex.exceptions.OnErrorNotImplementedException: Some Error
at io.reactivex.internal.functions.Functions$OnErrorMissingConsumer.accept(Functions.java:704)
at io.reactivex.internal.functions.Functions$OnErrorMissingConsumer.accept(Functions.java:701)
at io.reactivex.internal.observers.LambdaObserver.onError(LambdaObserver.java:74)
at io.reactivex.internal.operators.observable.ObservableCreate$CreateEmitter.tryOnError(ObservableCreate.java:85)
at io.reactivex.internal.operators.observable.ObservableCreate$CreateEmitter.onError(ObservableCreate.java:73)
at com.qjatjr1108.reactivejava.chapter07.ExceptionHandling.lambda$0(ExceptionHandling.java:15)
at io.reactivex.internal.operators.observable.ObservableCreate.subscribeActual(ObservableCreate.java:40)
at io.reactivex.Observable.subscribe(Observable.java:10903)
at io.reactivex.Observable.subscribe(Observable.java:10889)
at io.reactivex.Observable.subscribe(Observable.java:10792)
at com.qjatjr1108.reactivejava.chapter07.ExceptionHandling.cannotCatch(ExceptionHandling.java:21)
발생한 에러인 OnErrorNotImplementedException의 발생원인을 파악하기 위해 RxJava의 내부 소스코드를 살펴보겟습니다 .
먼저 at io.reactivex.Observable.subscribe(Observable.java:10792) (https://goo.gl/FvCYdQ) 의
public final Disposable subscribe() {
return subscribe(Functions.emptyConsumer(), Functions.ON_ERROR_MISSING, Functions.EMPTY_ACTION, Functions.emptyConsumer());
}
부분을 보게되면 앞서 subscribe() 함수를 호출할 때는 onNext 인자만 넘겼지만 내부에서는 두 번째 인자로 Functions.ON_ERROR_MISSING을 입력했습니다.
아래코드는 Functions.java(https://goo.gl/SQUtpa) 의
public static final Consumer<Throwable> ON_ERROR_MISSING = new OnErrorMissingConsumer();
static final class OnErrorMissingConsumer implements Consumer<Throwable> {
@Override
public void accept(Throwable error) {
RxJavaPlugins.onError(new OnErrorNotImplementedException(error));
}
}
Colored by Color Scripter
굵은 글씨를 보게되면 단지 OnErrorNotImpletementdException 예외를 던지라고 알려주는 역할을 합니다.
마지막으로 LamdaObserver.java(https://goo.gl/zxh2dC) 에있는 LamdaObserver.OnError()의 소스코드를 살펴보겠습니다 여기를 보면 왜 try-catch부분이 동작하지 않는지 알 수 있습니다.
@Override
public void onError(Throwable t) {
if (!isDisposed()) {
lazySet(DisposableHelper.DISPOSED);
try {
onError.accept(t);
} catch (Throwable e) {
Exceptions.throwIfFatal(e);
RxJavaPlugins.onError(new CompositeException(t, e));
}
}
}
위에서 onError 변수는 맨 처음에 subscribe() 함수에서 전달했던 ON_ERROR_MISSING입니다. 우리가 observable을 생성하여 onError(new Exception("Some Error"))를 호출하면 결국 subscribe(onNext, onError) 함수의 인자로 넘긴 onError 인자로 전달합니다.
따라서 try-catch 문은 RxJava에서 활용할 수 없습니다. 추가로 함수 체인이나 Observable내부에서 예외가 발생해도 onError 이벤트가 발생하고 try-catch 문으로는 해결할 수없습니다 .
즉, RxJava 에서 다른 방식의 예외처리 방법을 제공한다는것을 알 수 있습니다.
onErrorReturn() 함수
RxJava 에서는 에러도 어떠한 데이터로 보는것이 적절합니다. 따라서 예외 처리를하는 첫 번째 방식은 예외가 발생했을 때 에러를 의미하는 다른 데이터로 대체하는 것입니다.
onError 이벤트는 흐름이 바로 중단되므로 subscribe() 함수를 호출할 때 onError 이벤트를 처리하는 것은 Out Of Memory 같은 중대한 에러가 발생했을 때만 활용합니다.
onErrorReturn () 는 에러가 발생했을 때 내가 원하는 데이터로 대체하는 함수입니다.
onErrorReturn() 함수의 마블다이어그램
출처 - http://reactivex.io/RxJava/javadoc/io/reactivex/Observable.html
앞의 3개의 데이터가 정상적으로 발행되고 마지막 데이터에서 어떤 에러가 발생하는 경우 onErrorReturn() 함수는 인자로 넘겼던 기본값을 대신 발행하고 onComplete 이벤트가 발생합니다. onError 이벤트는 발생하지 않습니다.
onErrorReturn() 함수의 활용 예
String[] grades={"70","88","&100","93","83"}; //&100이 에러데이터
Observable<Integer>source= Observable.fromArray(grades)
.map(data->Integer.parseInt(data))
.onErrorReturn(e->{
if(e instanceof NumberFormatException){
e.printStackTrace();
}
return -1;
});
source.subscribe(data->{
if(data<0){Log.e("Wrong Data found");
return;
}
Log.i("Grade is : " +data);
});
먼저 Integer.parseInt() 메소드는 NumberFormatException 라고 하는 검증된 예외가 있습니다.
따라서 이를 처리해 주어야 합니다. RxJava 에서는 try-catch 문이 동작하지 않기 때문에 onErrorReturn() 함수에서 처리하며 NumberFormatException 발생시 -1을 리턴합니다.
subscribe() 함수는 성적 데이터를 처리하므로 0 보다 커야합니다.
onErrorReturn() 함수에서 예외 발생 시 음수값을 리턴했으므로 data가 0보다 작으면 에러 발생으로 판단하고 에러로그를 출력합니다.
위 코드를 보고 'onError 이벤트에서 예외처리를하는것과 무었이 다른가? '라고 생각할 수 있습니다. subscribe () 함수를 호출할 때 onError이벤트를 처리하는 것이 아닌 onErrorReturn()등의 함수를 활용하여 예외처리하는 것은 몇가지 장점이 있습니다.
1) 예외 발생이 예상되는 부분을 선언적으로 처리할 수 있습니다.
2) Observable을 생성하는 측과 구독하는 측이 서로 다를 수 있다는 점입니다.
구체적으로 말하자면 Observable에서 에러 가능성을 명시하지 않았는데 구독자가 필요한 예외처리를 빠짐없이 하는 것은 어렵다는 뜻입니다. 이럴 떄 Observable을 생성하는 측에서 발생할 수 있는 예외처리를 미리 해 두면 구독자는 선언된 예외상황을 보고 그에 맞는 처리를 할 수 있습니다. 예를들어 위 코드에서는 map(data->Integer.parseInt(data)) 부분에서 예외가 발생할 수 있음을 명시하고 있습니다.
아래 코드는 위의 코드를 onError 이벤트에서 예외처리하도록 변경한 것입니다.
구독할때 onError 이벤트에서의 예외처리
String[] grades={"70","88","&100","93","83"}; //&100이 에러데이터
Observable<Integer>source= Observable.fromArray(grades)
.map(data->Integer.parseInt(data));
source.subscribe(data->
Log.i("Grade is : " +data),
e->{if(e instanceof NumberFormatException){
e.printStackTrace();
}
Log.e("Wrong Data found");
});
}
실행결과는 위의 코드와 동일합니다
main | value = Grade is : 70
main | value = Grade is : 88
java.lang.NumberFormatException: For input string: "&100"
at java.lang.NumberFormatException.forInputString(Unknown Source)
at java.lang.Integer.parseInt(Unknown Source)
at java.lang.Integer.parseInt(Unknown Source)
at com.qjatjr1108.reactivejava.chapter07.ExceptionHandling.lambda$4(ExceptionHandling.java:50)
at io.reactivex.internal.operators.observable.ObservableMap$MapObserver.onNext(ObservableMap.java:59)
at io.reactivex.internal.operators.observable.ObservableFromArray$FromArrayDisposable.run(ObservableFromArray.java:107)
at io.reactivex.internal.operators.observable.ObservableFromArray.subscribeActual(ObservableFromArray.java:36)
at io.reactivex.Observable.subscribe(Observable.java:10903)
at io.reactivex.internal.operators.observable.ObservableMap.subscribeActual(ObservableMap.java:33)
at io.reactivex.Observable.subscribe(Observable.java:10903)
at io.reactivex.Observable.subscribe(Observable.java:10889)
at io.reactivex.Observable.subscribe(Observable.java:10818)
at com.qjatjr1108.reactivejava.chapter07.ExceptionHandling.onError(ExceptionHandling.java:51)
at com.qjatjr1108.reactivejava.chapter07.ExceptionHandling.main(ExceptionHandling.java:64)
main | error = Wrong Data found
onErrorReturnItem() 함수
한편 onErrorReturn() 함수와 비슷한 함수로 onErrorReturnItem() 함수가 있습니다.
onErrorReturn()함수와 동일하지만 Throwable 객체를 인자로 전달하지 않기 때문에 코드는 좀 더 간결해집니다. 단, 예외의 종류는 확인할 수 없습니다.
onErrorReturnItem() 함수의 활용 예
String[] grades={"70","88","&100","93","83"}; //&100이 에러데이터
Observable<Integer>source= Observable.fromArray(grades)
.map(data->Integer.parseInt(data))
.onErrorReturnItem(-1);
source.subscribe(data->{
if(data<0){Log.e("Wrong Data found");
return;
}
Log.i("Grade is : " +data);
});
Throwable( e)객체를 사용하지 않았기 떄문에 코드의 가독성이 좀 더 높아집니다.
실행결과
main | value = Grade is : 70
main | value = Grade is : 88
main | error = Wrong Data found
onErrorResumeNext() 함수
onErrorReturn()과 onErrorReturnItem() 함수는 에러가 발생한 시점에 특정 값으로 대체하는 것이였습니다. onErrorResumeNext() 는 에러가 발생했을 때 내가 원하는 Observable로 대체하는 방법입니다. Observable로 대체한다는 것은 에러 발생 시 데이터를 교체하는 것뿐만 아니라 관리자에게 이메일을 보낸다던가 자원을 해제하는 등의 추가 작업을 해야할 때 유용합니다.
onErrorResumeNext() 함수의 마블 다이어 그램
출처 - http://reactivex.io/RxJava/javadoc/io/reactivex/Observable.html
에러가 발생했을 때 특정 값을 발행한다는 점은 onErrorReturn() 함수와 크게 다르지 않습니다.
특정 값을 원하는 Observable로 설정할 수 있다는 점이 다릅니다.
onErrorResumeNext() 함수의 활용 예
String[] salesData={"100","200","A300"}; //A300은 에러데이터
Observable<Integer> onParseError=Observable.defer(()->{
Log.d("Send email to administrator");
return Observable.just(-1);
}).subscribeOn(Schedulers.io()); // IO 스케줄러에서 실행됨
Observable<Integer> source=Observable.fromArray(salesData)
.map(Integer::parseInt)
.onErrorResumeNext(onParseError);
source.subscribe(data->{
if(data<0){
Log.e("Wrong Data found");
return;
}
Log.i("Sales data :"+ data);
});
CommonUtils.sleep(1000);
에러가 발생했을 때 관리자에게 이메일을 보내고 '-1' 이라는 데이터를 발행하는 Obsevable로 대체합니다. onParseError 변수는 subscribeOn() 함수를 호출하여 IO 스케줄러에서 실행합니다.
RxJava는 이처럼 내가 원하는 코드를 실행하는 스케줄러를 선언적으로 지정할 수 있어
활용 범위가 넓습니다
실행결과
main | value = Sales data :100
main | value = Sales data :200
RxCachedThreadScheduler-1 | debug = Send email to administrator
RxCachedThreadScheduler-1 | error = Wrong Data found
참고로 onErrorResumeNext() 함수는 onErrorReturn () 함수처럼 Throwable을 받아오는 오버로딩도 제공합니다.
댓글