Spring WebFlux 예외 발생 및 예외 핸들링 함수 총정리: 실전 패턴과 완벽 가이드
Spring WebFlux에서 예외는 비동기 스트림 환경에서 다뤄야 하므로 기존 MVC와는 다른 패턴과 함수가 필요합니다. 본 포스트는 WebFlux에서 임의 예외를 발생시키는 방법과, 실무에서 자주 쓰는 예외 핸들링 함수(onErrorReturn, onErrorResume, doOnError 등)를 Kotlin 코드와 함께 상세하게 설명합니다. 실전 예제, 구조적 설명, 실무 팁, 공식 문서 및 참고자료까지 모두 포함합니다.
WebFlux에서 예외란 무엇인가?
Spring WebFlux는 리액티브 스트림(Flux, Mono) 기반의 논블로킹 환경이므로, 예외 발생과 처리가 동기 방식과 다릅니다. try-catch가 아닌 스트림 연산자(onErrorReturn, onErrorResume 등)를 활용해 예외를 처리하며, 컨트롤러/핸들러/라우터 레벨에서 다양한 방식으로 예외를 핸들링할 수 있습니다.
WebFlux 예외의 주요 특징
- 예외가 발생해도 스트림은 멈추지 않고, 대체 데이터 또는 새로운 스트림으로 이어질 수 있음
- try-catch는 스트림 내부에서 동작하지 않음
- 예외 발생 시 onError 계열 연산자를 통해 처리
- 글로벌 예외 처리(ControllerAdvice)도 지원
임의 예외 발생 방법
WebFlux에서 예외를 임의로 발생시키는 대표적인 방법은 다음과 같습니다.
// 1. map, flatMap 내부에서 예외 발생
Flux.range(1, 5)
.map { if (it == 3) throw IllegalArgumentException("3은 허용되지 않음") else it }
.subscribe(
{ println("onNext: $it") },
{ error -> println("onError: ${error.message}") }
)
// 2. Mono.error/Flux.error로 명시적 예외 스트림 생성
val errorMono = Mono.error<String>(RuntimeException("강제 예외 발생!"))
errorMono.subscribe(
{ println("onNext: $it") },
{ error -> println("onError: ${error.message}") }
)
실전 예시: 임의 예외 발생 컨트롤러
@RestController
@RequestMapping("/api/error")
class ErrorDemoController {
@GetMapping("/fail")
fun fail(): Mono<String> = Mono.error(RuntimeException("임의 에러 발생!"))
@GetMapping("/random")
fun random(): Mono<String> = Mono.just((1..5).random())
.map { if (it == 3) throw IllegalStateException("3은 지원하지 않음") else "ok: $it" }
}
WebFlux 예외 핸들링 함수들
1. onErrorReturn
- 예외 발생 시 지정한 기본값으로 대체
Flux.just(1, 2, 0)
.map { 10 / it }
.onErrorReturn(-1)
.subscribe { println("결과: $it") } // 10, 5, -1 출력
2. onErrorResume
- 예외 발생 시 대체 스트림(Flux/Mono)으로 전환
Mono.just("test")
.flatMap { Mono.error<String>(RuntimeException("실패!")) }
.onErrorResume { e ->
println("에러 발생: ${e.message}")
Mono.just("대체 데이터")
}
.subscribe { println("최종 결과: $it") }
3. doOnError
- 예외 발생 시 로그 출력, 알림 등 부가 작업 수행(스트림 종료X)
Flux.just("A", null, "C")
.map { it!!.lowercase() }
.doOnError { e -> println("에러 로그: ${e.message}") }
.onErrorReturn("에러 발생시 기본값")
.subscribe { println("결과: $it") }
4. onErrorMap
- 예외를 다른 예외로 변환(예: 사용자 정의 예외)
Mono.just("fail")
.flatMap { Mono.error<String>(IllegalArgumentException("잘못된 입력")) }
.onErrorMap { e -> CustomException("사용자 정의: ${e.message}") }
.subscribe(
{ println("onNext: $it") },
{ error -> println("onError: ${error.message}") }
)
class CustomException(message: String): RuntimeException(message)
실전: 예외 핸들링 전체 예제 (Kotlin)
@RestController
@RequestMapping("/api/handle")
class ExceptionController {
@GetMapping("/safe-div")
fun safeDiv(@RequestParam a: Int, @RequestParam b: Int): Mono<String> =
Mono.just(b)
.map { a / it }
.map { "결과: $it" }
.onErrorResume { e -> Mono.just("0으로 나눌 수 없습니다: ${e.message}") }
@GetMapping("/user/{id}")
fun getUser(@PathVariable id: Int): Mono<String> =
Mono.just(id)
.flatMap { if (it == 0) Mono.error(IllegalArgumentException("ID 0은 허용 안됨")) else Mono.just("User$it") }
.onErrorReturn("존재하지 않는 사용자")
}
예제 설명
/safe-div
: 0으로 나눌 때 onErrorResume으로 에러 메시지 반환/user/{id}
: ID가 0이면 onErrorReturn으로 기본 메시지 반환
글로벌 예외 처리 (ControllerAdvice)
WebFlux에서도 @RestControllerAdvice
로 글로벌 예외 처리가 가능합니다.
@RestControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(Exception::class)
fun handle(e: Exception): ResponseEntity<String> =
ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("글로벌 에러: ${e.message}")
}
예외 핸들링 함수별 비교 표
함수명 | 설명 | 예시 코드 |
---|---|---|
onErrorReturn | 예외 발생 시 기본값 반환 | .onErrorReturn(-1) |
onErrorResume | 예외 발생 시 대체 스트림 반환 | .onErrorResume { ... } |
doOnError | 예외 발생 시 부가 작업 | .doOnError { ... } |
onErrorMap | 예외 변환 | .onErrorMap { ... } |
WebFlux 예외 핸들링 실전 팁
- try-catch가 아닌 onError 계열 연산자를 적극 활용
- 예외 발생 시 로그, 모니터링, 알림 연동 등 부가 작업은 doOnError에서 처리
- onErrorResume으로 에러 상황에 맞는 대체 데이터/스트림 제공
- 글로벌 예외 처리는 RestControllerAdvice로 일관성 있게 관리
- 예외 메시지 노출은 보안상 주의(내부 정보 노출X)
결론 및 도움말
WebFlux에서 예외 처리는 스트림 연산자를 적극적으로 활용하는 것이 핵심입니다. 실전에서는 onErrorReturn, onErrorResume, doOnError 등을 적절히 조합해, 안정적이고 예측 가능한 리액티브 서버를 구현할 수 있습니다. 공식 문서와 다양한 샘플 코드를 참고하여, 자신만의 예외 처리 패턴을 만들어보세요.