Spring Framework/RestTemplate

RestTemplate Error Handling - RestTemplate Hands On 6

Junuuu 2023. 9. 27. 00:01
반응형

개요

이전 글들에서는 RestTemplate의 Get, Post, Delete 등에 대해 알아보았습니다.

이번 글에서는 RestTemplate Error Handling에 대해 알아보고자 합니다.
에러는 매우 다양하게 발생할 수 있습니다.

일시적인 네트워크 에러, 호출하는 서버의 다운, 잘못된 요청 등등..

 

에러를 발생시키는 코드

@RestController
class RestTemplateErrorHandling {

    val baseUrl = "localhost:8080"
    @GetMapping("/error-handle")
    fun errorHandle(){
        val apiUrl = "/error-handle-test"
        val restTemplate = RestTemplate()
        val responseEntity = restTemplate.getForEntity(
            baseUrl + apiUrl,
            String::class.java
        )
        println(responseEntity)
    }

    @GetMapping("/error-handle-test")
    fun errorHandleTest(){
        throw IllegalArgumentException("호출시 에러가 발생합니디")
    }
}

간단한 get 호출과 이를 호출할 때 Exception이 발생하도록 코드를 구성해 보았습니다.

결과는 어떻게 될까요?

 

결과

org.springframework.web.client.HttpServerErrorException$InternalServerError: 500 : 
"{"timestamp":"2023-09-04T13:27:44.573+00:00","status":500,"error":"Internal Server Error",

HttpServerErrorException이 발생합니다.

 

기본적으로 RestTemplate은 HTTP 오류가 발생하면 다음과 같은 예외를 발생시킵니다.

  • HttpClientErrorException - 4xx 상태코드
  • HttpServerErrorException - 5xx 상태코드
  • UnkownHttpStatusCodeException - 알 수 없는 HTTP 상태

위의 예외들은 모두 RestClientResponseException을 상속받고 있습니다.

 

DefaultResponseErrorHandler

RestTemplate은 에러처리를 위해 기본적으로 DefaultResponseErrorHandler를 활용합니다.

 

DefaultResponseErrorHandler는 ResponseErrorHandler 인터페이스를 구현하고 있습니다.

public interface ResponseErrorHandler {

	/**
	 * Indicate whether the given response has any errors.
	 * <p>Implementations will typically inspect the
	 * {@link ClientHttpResponse#getStatusCode() HttpStatus} of the response.
	 * @param response the response to inspect
	 * @return {@code true} if the response indicates an error; {@code false} otherwise
	 * @throws IOException in case of I/O errors
	 */
	boolean hasError(ClientHttpResponse response) throws IOException;

	/**
	 * Handle the error in the given response.
	 * <p>This method is only called when {@link #hasError(ClientHttpResponse)}
	 * has returned {@code true}.
	 * @param response the response with the error
	 * @throws IOException in case of I/O errors
	 */
	void handleError(ClientHttpResponse response) throws IOException;

	/**
	 * Alternative to {@link #handleError(ClientHttpResponse)} with extra
	 * information providing access to the request URL and HTTP method.
	 * @param url the request URL
	 * @param method the HTTP method
	 * @param response the response with the error
	 * @throws IOException in case of I/O errors
	 * @since 5.0
	 */
	default void handleError(URI url, HttpMethod method, ClientHttpResponse response) throws IOException {
		handleError(response);
	}

}

JavaDoc을 읽어보면 ClientHttpReponse의 StatusCode를 기반으로 에러를 판단하는 hasError 메서드가 존재하고, 만약 에러라고 판단한다면 에러를 어떻게 처리할지 수행하는 handleError 메서드가 존재합니다.

 

 

RestTemplate에서부터 디버깅

doExecute메서드내부를 살펴보면 다음과 같은 코드를 호출합니다.

response = request.execute();
observationContext.setResponse(response);
handleResponse(url, method, response);

 

여기서 handleResponse를 호출합니다.

protected void handleResponse(URI url, HttpMethod method, ClientHttpResponse response) throws IOException {
		ResponseErrorHandler errorHandler = getErrorHandler();
		boolean hasError = errorHandler.hasError(response);
		if (logger.isDebugEnabled()) {
			try {
				HttpStatusCode statusCode = response.getStatusCode();
				logger.debug("Response " + statusCode);
			}
			catch (IOException ex) {
				logger.debug("Failed to obtain response status code", ex);
			}
		}
		if (hasError) {
			errorHandler.handleError(url, method, response);
		}
	}

errorHandler를 가져옵니다 (DeafultErrorHandler)

그리고 errorHandler가 에러를 가지고 있다면 에러를 처리하고 그렇지 않은 경우에는 별다른 작업을 수행하지 않습니다.

 

에러를 판단하는 hasError 로직

public boolean hasError(ClientHttpResponse response) throws IOException {
		HttpStatusCode statusCode = response.getStatusCode();
		return hasError(statusCode);
	}

statusCode를 가져와서 hasError 메서드를 다시 호출합니다.

 

더 들어가 보면 HttpStatus Enum에서 isError 메서드를 구현합니다.

@Override
public boolean isError() {
	return (is4xxClientError() || is5xxServerError());
}

400과 500에 대한 에러를 처리합니다.

 

handleError 메서드를 들어가보면 다음과 같습니다.

protected void handleError(ClientHttpResponse response, HttpStatusCode statusCode) throws IOException {
		String statusText = response.getStatusText();
		HttpHeaders headers = response.getHeaders();
		byte[] body = getResponseBody(response);
		Charset charset = getCharset(response);
		String message = getErrorMessage(statusCode.value(), statusText, body, charset);

		RestClientResponseException ex;
		if (statusCode.is4xxClientError()) {
			ex = HttpClientErrorException.create(message, statusCode, statusText, headers, body, charset);
		}
		else if (statusCode.is5xxServerError()) {
			ex = HttpServerErrorException.create(message, statusCode, statusText, headers, body, charset);
		}
		else {
			ex = new UnknownHttpStatusCodeException(message, statusCode.value(), statusText, headers, body, charset);
		}

		if (!CollectionUtils.isEmpty(this.messageConverters)) {
			ex.setBodyConvertFunction(initBodyConvertFunction(response, body));
		}

		throw ex;
	}

4xx인 경우에는 HttpClientErrorException을 반환하고

5xx인 경우에는 HttpServerErrorException을 반환합니다.

그 외에는 UnkownHttpStatusCodeException을 반환합니다.

 

 

에러를 어떻게 처리하는 게 좋을까?

 

두 가지 방안이 떠오릅니다.

  • try-catch로 잡아서 처리하기
  • ResponseErrorHandler를 구현하여 DefaultErrorHandler 대체하기

DeafultErrorHandler를 구현하여 내가 원하는 Exception을 반환하도록 할 수 있습니다.

하지만 조금 더 직관적인 코드를 생각해 보았을 때는 try-catch가 더 좋아 보입니다.

예를 들어 RestTemplate을 잘 모르는 개발자들은 DefaultErrorHandler의 존재를 모를 수 있다고 생각합니다.

이런 상황에서 다른 구현체가 사용되는 것이 어색하고 하나의 허들이 될 수 있습니다.

 

    @GetMapping("/error-handle")
    fun errorHandle(): String{
        val apiUrl = "/error-handle-test"
        val restTemplate = RestTemplate()
        try{
            val responseEntity = restTemplate.getForEntity(
                baseUrl + apiUrl,
                String::class.java
            )
            println("----정상 호출 ----")
            println(responseEntity)
            return "성공"
        } catch (e: HttpClientErrorException){
            println("----4xx Http Status 에러가 발생했습니다, 요청값에 문제가 없는지 확인해주세요 ----")
            return "실패"
        } catch (e: HttpServerErrorException){
            println("----5xx Http Status 에러가 발생했습니다 ----")
            return "실패"
        } catch (e: Exception){
            println("---- 예상치 못한 에러입니다, 분석 후 대응이 필요합니다----")
            return "실패"
        }
    }

 

try-catch를 통해 상세하게 400번대 에러인지 500번째 에러인지를 구분할 수 있습니다.

그리고 해당값의 반환값을 통해 성공인지 실패인지를 전달해 준다면 예외의 전파 없이 Client 호출부를 활용하는 Application 로직에서 다음 행위를 판단할 수 있습니다.

 

예를 들어 다음과 같은 코드가 가능합니다.

if(clientLogicResponse.isError()){
	...에러에 대한 헨들링을 알아서 처리하세요.
}

 

 

참고자료

https://www.baeldung.com/spring-rest-template-error-handling