ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • RestTemplate Error Handling - RestTemplate Hands On 6
    Spring Framework/RestTemplate 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

     

    댓글

Designed by Tistory.