ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • String MVC - 직접 구현해보는 API 예외처리
    Spring/Spring MVC 2022. 2. 14. 01:25
    728x90
    반응형

     

     

     

     

     

     

    API 예외처리

     

     text/html 형식의 예외처리를 직접 구현하여 사용하면 예외 발생 시 응답 메시지로 4xx.html, 5xx.html와 같은 오류 페이지를 쉽게 클라이언트에게 보내줄 수 있다. 하지만 API의 경우에는 다르다. API 예외처리에서 응답 메시지에 오류 페이지를 전달해주면 JSON형식이 아니기 때문에 제대로 된 랜더링을 하지 못하게 된다. 때문에 각 오류 상황에 맞게 오류에 대한 정보들을 JSON으로 전달해줘야 한다.

     

     스프링에서는 @ExceptionHandler 애노테이션을 제공하여 편리하게 사용할 수 있지만 @ExceptionHandler를 사용하기전 어떻게 응답 메시지를 JSON형식으로 전달해야하는지 처음부터 직접 구현해보면서 알아보자.

     

    참고
    API 예외 처리를 할 떄는 @ExceptionHandler를 사용한다.
    따라서 아래 내용들은 어떻게 API 예외 처리를 하는지 공부 목적으로 보길 바란다.

     

    APi 예외 처리 직접 구현하기

     API를 요청시 오류가 발생하면 JSON 형식으로 오류 정보를 전달해야한다. JSON 형식으로 오류 정보를 전달할 수 있도록 직접 구현한 ErrorPageController에 JSON 응답을 할 수 있도록 구현해야한다.

     

        @RequestMapping(value = "/error-page/500", produces = MediaType.APPLICATION_JSON_VALUE)
        public ResponseEntity<Map<String, Object>> errorPage500Api(HttpServletRequest request, HttpServletResponse response) {
    
            log.info("API errorPage 500");
    
            Map<String, Object> result = new HashMap<>();
            Exception ex = (Exception) request.getAttribute(ERROR_EXCEPTION);
            result.put("status", request.getAttribute(ERROR_STATUS_CODE));
            result.put("message", ex.getMessage());
    
            Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
            return new ResponseEntity<>(result, HttpStatus.valueOf(statusCode));
        }

    produces = MediaType.APPLICATION_JSON_VALUE
           - HTTP Header의 Accept 값이 application/json일 때 해당 메서드 호출

           - 즉 클라이언트가 받고 싶은 타입이 JSON일 때 해당 컨트롤이 호출되어 JSON형식으로 전달

    Map<String, Object> result = new HashMap<>();

           - JSON 형식으로 전달하기 위해 개발자가 직접 오류 정보를 Map에 저장

    return new ResponseEntity<>(result, HttpStatus.valueOf(statusCode));

           - 개발자가 직접 오류 정보를 저장한 Map과 상태코드를 JSON형태로 전달

     

    응답 결과

     

     위 응답 결과를 보면 개발자가 직접 저장한 오류 정보(status, message)가 JSON 형태로 출력된 것을 볼 수 있다.

     


     

     

    API 예외 처리 - BasicErrorController 이용

     API 예외 처리도 스프링 부트가 제공하는 기본 예외 처리 방식을 사용할 수 있다. 스프링 부트가 제공하는 BasicErrorController 클래스를 살펴보면 다음과 같은 메서드가 존재한다.

     

    @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
        HttpStatus status = getStatus(request);
        Map<String, Object> model = Collections
                .unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
        response.setStatus(status.value());
        ModelAndView modelAndView = resolveErrorView(request, response, status, model);
        return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
    }
    
    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        HttpStatus status = getStatus(request);
        if (status == HttpStatus.NO_CONTENT) {
            return new ResponseEntity<>(status);
        }
        Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
        return new ResponseEntity<>(body, status);
    }

     

     두 메서드를 보면 익숙한 느낌이 들것이다. 바로 위에서 구현한 API 예외 처리방식으로 BasicErrorController 클래스에서 구현이 되어있다. BasicErrorController에서 예외 처리를 할 때 HTTP Header의 Accept가 text/html 형식이면 errorHtml메서드가 호출이 되어 에러 페이지를 전달하고, JSON 형식이면 error 메서드가 호출이 되어 ResponseEntity 클래스를 이용하여 HTTP Body에 JSON 형식으로 데이터를 전달한다.

     

    API 예외 발생시 응답 결과

     

     하지만 이 방법은 각 컨트롤러의 예외마다 다른 응답 결과를 출력해야 할 경우에는 적합하지 않다. 따라서 BasicErrorController를 사용할 때는 예외 처리로 오류 페이지를 전달 할 때만 사용하고 API 예외 처리는 다른 방법을 사용하는게 좋다. (다시 한번말하지만 API 예외 처리는 @ExceptionHandler를 사용하므로 API 예외 처리가 어떻게 처리되는지 이해하기 위한 참고로만 보자)

     


     

     

    API 예외 처리 - HandlerExceptionResolver 이용한 예외 처리 구현

     스프링 부트에서 기본으로 제공하는 예외 처리를 사용하면 문제점이 있다. 만약 클라이언트가 잘못된 전달인자를 넘기면 클라이언트의 잘못이므로 HTTP 상태코드는 400으로 처리되어야 한다. 하지만 BasicErrorController 사용하면 예외가 발생하는 곳은 서버에서 발생하기 때문에 500 상태코드를 저장하게 되며 클라이언트에게도 500 상태코드를 전달하게 된다.

     

     이런 문제를 해결하기위해 스프링 MVC는 컨트롤러(핸들러) 밖으로 예외가 던져진 경우 예외를 해결하고, 동작과 오류 정보들을 새로 정의할 수 있는 HandlerExceptionResolver를 제공한다(줄여서 ExceptionResolver라 한다. 

     

     

    참고 ExceptionResolver로 예외를 해결해도 postHandler은 호출되지 않는다 

     

    HandlerExceptionResolver의 사용

     

    • 발생한 예외를 처리하기 위해 HandlerExceptionResolver 인터페이스의 구현 클래스로
      사용자 정의 ExceptionResolver를 구현한다
    • 구현한 ExceptionResolver에서 예외 처리, 동작과 오류 정보를 정의한다.
    • 예외를 처리하면 ModelAndView를 반환하여 WAS가 정상 흐름으로 처리한다.
    • ExceptionResolver에서 예외를 처리하기 위해 구현한 메서드들을 하나씩 호출하여 
      해당 예외를 처리할 수 있는 메서드를 찾는다.

     

    @Slf4j
    public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
    
        @Override
        public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
    
            log.info("call resolver", ex);
    
            try {
                if (ex instanceof IllegalArgumentException) {
                    log.info("IllegalArgumentException resolver to 400");
                    response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
                    return new ModelAndView();
                }
    
            } catch (IOException e) {
                log.error("resolver ex", e);
            }
    
            return null;
        }
    }

    respose.sendError(상태코드, 에러메시지)
            - 서블릿에서 상태 코드에 따른 오류를 처리하도록 위임

            - WAS는 오류에 따른 오류 페이지를 내부에 요청을 하여 찾는다. 

    return new ModelAndView()

            - 빈 ModelAndView : 빈 객체를 반환하면 뷰를 렌더링 하지 않고, 정상 흐름으로 처리

            - ModelAndView 설정 : View, Model 등의 정보를 설정하여 반환하면 뷰를 렌더링하여 처리

            - null : null을 반환하면 다음 ExceptionResolver를 찾아서 실행
                     예외 처리 가능한 ExceptionResolver가 없으면 서블릿에게 예외가 전달된다.


     

    ExceptionHandler 등록

     

    • 사용자 정의한 ExceptionHandler를 사용하기 위해서 등록을 해줘야한다.
    • resolvers.add 를 통해 ExceptionHandler를 등록할 수 있다.

     

    @Configuration
    public class WebConfig implements WebMvcConfigurer {
    
        ...
    
        @Override
        public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
            resolvers.add(new MyHandlerExceptionResolver());
        }
        
        ...
    
    }

     

    HandlerExceptionResolver에서 예외를 마무리하기

     

     위에서 구현한 HandlerExceptionResolver가 처리하는 과정을 보면 예외가 발생시 결국은 WAS까지 예외가 던져지고, WAS에서 오류 페이지 정보를 찾기 위해 다시 재호출하는 과정을 거친다. 이런 복잡한 과정(WAS의 내부 호출)을 줄이기위해 HandlerExceptionResolver에서 직접 예외를 마무리 할 수 있다.

     

    @Slf4j
    public class UserHandlerExceptionResolver implements HandlerExceptionResolver {
    
        private final ObjectMapper objectMapper = new ObjectMapper();
    
        @Override
        public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
    
            try {
    
                if (ex instanceof UserException) {
                    log.info("UserException resolver to 400");
                    String acceptHeader = request.getHeader("accept");
                    response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
    
                    if ("application/json".equals(acceptHeader)) {
                        Map<String, Object> errorResult = new HashMap<>();
                        errorResult.put("ex", ex.getClass());
                        errorResult.put("message", ex.getMessage());
                        String result = objectMapper.writeValueAsString(errorResult);
    
                        response.setContentType("application/json");
                        response.setCharacterEncoding("utf-8");
                        response.getWriter().write(result);
                        return new ModelAndView();
                    } else {
                        // TEXT/HTML
                        return new ModelAndView("error/500");
                    }
                }
    
            } catch (IOException e) {
                log.error("resolver ex", e);
            }
    
            return null;
        }
    }

    ▶ HTTP Header에서 Accept 값에 따른 분기 처리를 하여 text/html 형식과 JSON 형식에 따른
       처리를 하였다

     

     위 코드를 보면 컨트롤러에서 발생한 예외를 ExceptionResolver에서 직접 예외를 처리하여 WAS에 예외가 전달되지 않고 예외 처리가 끝이 난 것을 볼 수 있다. 즉, ExceptionResolver에서 예외 처리가 된 후 WAS로 전달되면 WAS는 다시 내부 호출을 할 필요가 없어지게 된것이다.


     

    728x90
    반응형

    댓글

Designed by Tistory.