Spring WebFlux에서 Mono 또는 Flux를 처리하다 보면 Exception 등 Error가 발생하는 경우가 많습니다. 이 때 Exception을 throw하지 않고 Reactive Stream을 Graceful 하게 처리할 수 있는 다양한 방법과 Error 발생 시 취할 수 있는 Retry 전략에 대해 알아보도록 하겠습니다.
Error Handling
1. onErrorReturn
onErrorReturn은 Reactive Stream에서 Error가 발생했을 경우 정해진 Fallback value를 리턴해주는 방식입니다.
위와 같이 3가지 조건의 다른 parameter를 갖는 method가 정의되어 있습니다. 모든 Error에 대해 fallback value를 리턴하거나 / 특정 Error Type에 대해서만 처리하거나 / 특정 Predicate
에 부합될 경우에만 처리하는 등 각 상황에 맞게 전략적으로 사용할 수 있습니다.
예제 코드로 아래와 같이 확인해 보면
searchService.getPost(2)
를 호출 할 경우 RuntimeException
이 발생하고 정해진 Fallback value를 리턴하고 종료하는 것을 확인할 수 있습니다.
onErrorReturn
은 Reactive Stream 처리 도중 Error가 발생할 경우 정해진 Fallback value를 리턴하고 나머지 element를 처리하지 않고 stream을 종료합니다.
2. onErrorResume
onErrorResume은 Reactive Stream에서 Error가 발생했을 경우 Fallback publisher를 구독하도록 전환해주는 방식입니다.
onErrorResume
또한 3가지 조건의 parameter를 갖는 상황별 method가 정의되어 있습니다.
특정 Exception Type에 대한 처리 예제를 보면
searchService.getPost(2)
를 호출 할 경우 Exception
이 발생하고 Fallback Publisher를 subscribe하는 결과를 확인 할 수 있습니다.
onErrorResume
은 Reactive Stream 처리 도중 Error가 발생할 경우 정해진 Fallback Function
을 통해 새로운 Publisher로 대체하고 나머지 element는 처리하지 않고 stream을 종료합니다.
3. onErrorContinue
onErrorContinue
는 Reactive Stream에서 Error가 발생했을 경우 Error를 처리해 주고 남은 Stream element들을 계속 처리해주는 방식입니다.
onErrorContinue
또한 동일하게 3가지 조건의 parameter를 갖는 상황별 method가 정의되어 있으며, 이번에는 Predicate
에 대한 예제를 알아보겠습니다.
searchService.getPost(2)
를 호출 했을 경우 RuntimeException
이 발생하고, onErrorContinue
의 Predicate
조건에 따라 “Mock”이라는 단어가 포함되어 있으므로 Error를 logging하고 다음 Stream element를 처리합니다.
onErrorContinue
는 Reactive Stream 처리 도중 Error가 발생할 경우 Consumer
를 통해 Error를 처리하고 나머지 element를 계속 처리합니다.
Retry Strategy
1. retry
retry
는 Error
발생 시 Reactive Stream을 처음 부터 다시 subscribe 합니다. 하지만 retry
를 아무 조건없이 사용하게 되면 무한정 retry를 시도하기 때문에 대상 stream에 과부하가 걸릴 수 있습니다.
이런 이유 때문에 기본 retry
를 이용하기 보다는 최대 재시도 횟수를 제한하는 파라미터가 추가된 method를 주로 사용합니다.
retry(long numRetries)
를 이용하면 Reactive Stream에서 에러 발생 시 최대 numRetries만큼 재시도를 하고 그래도 에러가 발생하면 에러를 리턴하게 됩니다.
searchService.getPost(2)
호출 시 2번 에러가 나고 이후에 정상처리되게 설정하면 아래와 같이 두 번의 재시도 후 정상 처리되는 것을 확인 할 수 있습니다.
retry
를 이용하면 Reactive Stream에서 에러 발생 시 해당 Stream을 다시 Subscribe하여 재처리를 시도하며 최대 재시도 횟수를 설정 할 수 있습니다.
2. retryWhen
도식을 보면 복잡해 보이지만 retryWhen
은 retry
조건을 다양하게 주고 싶을 때 RetrySpec
을 통해 원하는 조건에 따른 재시도가 가능하게 해주는 방식입니다.
대표적으로 사용되는 3가지 RetrySpec에 대해 알아보겠습니다.
- Retry.max
Retry.max(long max)
는 retry(long numReties)
와 같은 기능을 합니다.
처리 결과를 보면 아래와 같이 두 번의 재시도 후에 정상 처리됨을 확인 할 수 있습니다.
- Retry.fixedDelay
Retry.fixedDelay(long maxAttemps, Duration fixedDelay)
는 정해진 Duration
동안 delay를 가진 후 maxAttempts 만큼 Retry
를 시도합니다.
위와 같이 2초의 delay와 최대 2회 재시도를 설정 했을 경우, 약 2초 간격으로 새로운 Scheduler
에서 재시도를 처리한 것을 확인 할 수 있습니다.
- Retry.backoff
Retry.backoff(long maxAttempts, Duration minBackoff)
는 Reactive Stream에서 에러가 발생 할 경우 최소 minBackoff 만큼 delay 후 maxAttemps 만큼 Retry
를 시도합니다. Retry.fixedDelay
와의 차이점은 에러가 계속 발생 할 경우 Delay 시간이 지수 형태로 점점 늘어난다는 점입니다.
실행 결과를 보면 delay 시간이 최소 2초에서 점점 늘어나는 것을 볼 수 있습니다.
견고한 어플리케이션 개발에서 중요한 것 하나가 Error 처리입니다. Reactive Stream에서 제공하는 onError
관련 메소드와 Retry
를 잘 조합하면 예상치 못한 에러 상황에서도 안정적으로 서비스를 제공 할 수 있습니다.
관련 소스는 아래 GitHub에서 확인 할 수 있습니다.