Spring boot에서 WebClient를 사용해 다른 서버로 REST api 요청을 보낼 때, io.netty.channel.ConnectTimeoutException이 발생하면 별도로 처리하고 싶었다.
webClient.get()
.uri(...)
.header(...)
.exchangeToMono(...)
.doOnError(error -> {
log("에러 발생 : " + error.getMessage());
})
.onErrorMap(error -> {
if (error instanceof io.netty.channel.ConnectTimeoutException) {
log("커넥션 타임 아웃 발생");
// custom 로직
}
return error; // 그 외의 예외는 그대로 반환
})
.block();
.doOnError의 log에서 io.netty.channel.ConnectTimeoutException이 발생하는 것을 보고, if문으로 error의 instanceof를 판단하여 ConnectTimeoutException들을 걸러낼 수 있을거라 생각했다.
그런데, 오이잉~?
if (error instanceof io.netty.channel.ConnectTimeoutException)에서 잡히지가 않았다?
log("에러 클래스: " + cause.getClass().getName()); 로 확인해보니 WebClientRequestException 클래스인 것을 볼 수 있었다.
이유는 Chained Exception이기 때문이다.
예외들은 자신의 원인이 된 예외 클래스를 cause라는 필드로 가질 수 있다.

그 동안 수많은 예외를 마주하면 보았던 저 Caused by:..........저게 다 예외 체이닝이었던 거임. 나만 이거 지금 알았냐 ????????

Exception들은 Throwable클래스를 상속받고 있는데, Throwable은 initCause와 getCause 메서드를 가지고 있다. 그렇기에 모든 예외 클래스들은 자신의 cause를 등록하고, 조회할 수 있다.
extends RuntimeException 처럼 상속으로도 예외들간의 관계를 표현할 수 있다. 하지만 상속은 각 예외들의 종류를 구분하고 묶기 위한 용도가 크고, 예외 체이닝은 어떤 예외가 발생한 근본적인 예외를 명시하여 디버깅을 더 쉽게 하도록 돕는데 의의가 있는 듯 하다.
보통 checked exception을 unchecked exception(runtime exception)으로 바꿔 던지는 걸 지향하는데, 예외 체이닝을 사용하면 상속 관계를 만들지 않고도 간편하게 checked exception을 unchecked exception으로 감쌀 수 있다는 장점이 있다.
// 수정 전 코드
.onErrorMap(error -> {
// 어림도 없지
if (error instanceof io.netty.channel.ConnectTimeoutException) {
log("커넥션 타임 아웃 발생");
// custom 로직
}
return error; // 그 외의 예외는 그대로 반환
})
// 수정1
.onErrorMap(error -> {
// cause를 뺌...
Throwable cause = error.getCause();
if (cause instanceof io.netty.channel.ConnectTimeoutException) {
log("커넥션 타임 아웃 발생");
// custom 로직
}
return error; // 그 외의 예외는 그대로 반환
})
// 수정2 while문으로 cause 돌리기 ... ...
.onErrorMap(error -> {
Throwable cause = error;
while (cause != null) {
// ConnectTimeoutException인지 확인
if (cause instanceof io.netty.channel.ConnectTimeoutException) {
log("커넥션 타임 아웃 발생");
// custom 로직
}
// 다음 원인으로 이동
cause = cause.getCause();
}
// ConnectTimeoutException이 없으면 원래 예외 반환
return error;
})