9. API 예외 처리

ys·2024년 1월 15일

Spring-mvc2

목록 보기
9/10

김영한 강사님의 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술을 듣고 정리한 내용입니다. 자세한 내용은 강의를 참고해주세요

  • HTML의 페이지에 대한 경우, BasicErrorController가 자동으로 등록해주어서, 우리는 에러 페이지만 /templates/error경로에 만들어 주면 된다.
  • 그런데 API 통신의 경우는??? -> json형태로 http body에 넣어서 전달해야 하는데??
  • 심지어 컨트롤러마다 다 다른 API 규칙을 정해서, 다른 값을 전달해야 한다...
  • 하나씩 천천히 알아보자

저번시간에 만든 컨트롤러를 사용하면, API로 요청해도, 오류가 발생하면 우리가 만들었던 HTML 오류페이지가 전달된다

API통신을 위한 새로운 컨트롤러를 만든다!

@RequestMapping(value = "/error-page/500",
produces = MediaType.APPLICATION_JSON_VALUE)
  • produces 파라미터를 이용해, http 요청 헤더의 Accept 헤더가 json타입일 때, 이 컨트롤러를 사용하게 정해줄 수 있다.
  • 그리고 이페이지 안에 Hash Map<>을 만들고, 이를 나중에 ResponseEntity를 사용해 응답하고
  • 메시지 컨버터가 동작하면서 클라이언트에게 JSON 형식으로 반환되게 된다
  • 이렇게 서블릿을 이용해되 되긴 하지만, 너무 귀찮고 기능이 부족하다
  • 이제부터 스프링 부트 오류 처리 기능을 알아보겠다

스프링 부트 기본 오류 처리

  • 일단 API처리도 스프링 부트가 제공하는 BasicErrorController을 이용할 수 있다

  • 다음은 BasicErrorController의 기능이다

  • errorHtml : produces = MediaType.TEXT_HTML_VALUE 로 클라이언트 요청의 Accept헤더 값이 text/html경우에, ModelAndView를 반환해 view화면을 제공한다

  • error : html외의 경우에 호출되고 ResponseEntity로 HTTP body 메시지에, JSON데이터를 반환한다

  • /error 페이지로 기본으로 요청하고 server.error.path 경로에서 수정 가능하다

이렇게 BasicErrorController을 이용하면, HTML페이지를 제공하는 경우에는 매우 편리하다(/error페이지에 view파일만 추가하면 된다).
그런데 API는 컨트롤러나 예외마다 다른 응답 결과를 출력해야 할 수 있다. API 오류처리는 @ExceptionHandler을 사용한다

다시 정리하자면

HTML 오류처리 : BasicErrorController
API 오류처리 : @ExceptionHandler

HandlerExceptionResolver 인터페이스

  1. 지금까지 우리는 예외가 발생 -> DispatcherServlet을 넘어 WAS서버에 예외가 전달되면
  2. WAS서버는 -> 서버 오류네~~ -> 500 Error를 제공했다
  3. 이제는 400,404 상태코드로 처리하고, 오류 메시지, 형식들을 API로 다르게 처리해보자!!!

if IllegalArgumentException -> 컨트롤러 밖으로 오류 발생

  • 서버오류(500)이 아닌
  • 클라이언트 문제인(bad reauest -> 400)오류를 만들고 싶다

  • 지금까지는, DispatcherServlet에서 WAS서버로 오류가 전달되면
  • WAS서버는 -> 서버 오류라고 정의하였다 -> 500 Error!!!

  • 이제는 HandlerExceptionResolverExceptionReosolver을 이용해 핸들러의 오류를 DispatcherServlet으로 와서
  • 바로 WAS서버로 보내는게 아니라, ExceptionResolver에서 한번 해결을 시도하고
  • 예외를 해결 성공하면 WAS 서버에 정상응답을 보내고
  • 예외를 해결 실패하면, 다음 ExceptionResolver 모두 시도해보고, 실패했다면
  • 처음에 핸들러에서 온 오류를 WAS 서버에 보내준다
  • 그러면 WAS 서버는 500오류가 날것이다..
@Slf4j
public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception 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;
    }
}
  • 인터페이스인, HandlerExceptionResolver을 구현하는 리졸버를 만들어보자!!!
  • 먼저 파라미터로 request,response그리고 오류를 처리해야되기 때문에 handlerException을 받는다
  • IOException 때문에 try,catch문을 사용한다
  • 핵심 코드를 보면 responsesendError서버 에러(500)가 아닌, 내가 보내고 싶은 error을 DispatcherServlet에 보내준다
  • 그리고 반환값은 ModelAndView이다
  • 오잉? 나는 API값을 반환하고 싶은데, 갑자기 화면까지 전달하는 ModelAndView를 왜 반환해야되지?
  • 이때 반환값은 3가지로 정해져있는데 -> DispathcerServlet의 처리 코드에서 자세히 볼 수 있다
  1. 빈 ModelAndView: new ModelAndView() 처럼 빈 ModelAndView 를 반환하면 뷰를 렌더링 하지 않고, 정상 흐름으로 서블릿이 리턴된다.
  2. ModelAndView 지정: ModelAndView 에 View , Model 등의 정보를 지정해서 반환하면 뷰를 렌더링 한다.
  3. null: null 을 반환하면, 다음 ExceptionResolver 를 찾아서 실행한다 만약 처리할 수 있는ExceptionResolver 가 없으면 예외 처리가 안되고, 기존에 발생한 예외를 서블릿 밖으로 던진다. -> 즉 서버 오류(500)

그러면 WAS 서버에서 sendError된 에러를 보고, BasicErrorController의 기본설정경로인 /error를 호출해, 오류를 처리한다!!!

  • 이렇게 하고 WebConfig 즉 @Configuration이 있는 파일에 extendHandlerExceptionResolvers를 오버라이드하고
  • 파라미터에 add(new 객체 )를 해줘야 한다
  • 이때 configureHandlerExceptionResolvers 를 사용하면 스프링이 기본으로 등록하는 ExceptionResolver 가 제거되므로 주의, extendHandlerExceptionResolvers 를 사용하자

  • 그런데, 예외를 다시 WAS에 던지고, 오류 페이지 정보를 다시 찾아서 /error를 호출하는 과정은 너무 복잡하다
  • 그리고 시간도 더 많이 걸린다,,,
  • 이때 ExceptionResolver을 이용하면, 예외가 발생했을 때, 이런 복잡한 과정 없이 한번에 처리할 수 있다
  • 아까, response도 파라미터로 받는다고 하였다
  • response에 오류 데이터를 담아서 DispatcherServler으로 보내버리는 것이다!!!
  • 그러면 응답 메시지이므로 WAS입장에서는 정상 처리가 된 것이고, 다시 컨트롤러로 오류 처리를 할 필요가 없는 것이다!!!
  • 즉 양방향에 걸친 처리를 한방향으로 줄일 수 있다
@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;
    }
}
  • 그런데,,, 코드만 봐도, ExceptionResolver 인터페이스를 바로 구현하려니 매우 복잡하다
  • 마치 서블릿으로 응답 데이터를 실어보내는 코드와 매우 유사하다,,,
  • response에 하나하나 담아주기는 힘들다..
  • 이제 스프링이 제공하는 ExceptionResovler을 알아보자!!!

스프링의 ExceptionResolver

  • 스프링 부트가 기본으로 제공하는 ExceptionResolver 는 다음과 같다.
  • HandlerExceptionResolver Composite 에 다음 순서로 등록

  • ExceptionHandlerExceptionResolver : 1순위, @ExceptionHandler 을 처리
  • ResponseStatusExceptionResolver : 2순위, HTTP상태 코드를 지정한다
  • DefaultHandlerExceptionResolver : 3순위, 스프링 내부 기본 예외를 처리한다
  • 이제 하나하나씩 알아보자!!!
  • 가장 중요한건 ExceptionHandlerResolver이다!!!

ResponseStatusResolver

  • ResponseStatusExceptionResolver 는 예외에 따라서 HTTP 상태 코드를 지정해주는 역할을 한다.
  • 다음 두 가지 경우를 처리한다.
    • @ResponseStatus 가 달려있는 예외
    • ResponseStatusException 예외

  • BadRequestException 예외가 컨트롤러 밖으로 넘어가면
  • ResponseStatusExceptionResolver 예외가 해당 애노테이션을 확인해서
  • 오류 코드를 HttpStatus.BAD_REQUEST (400)으로 변경하고, 메시지도 담는다.
  • 즉, response.sendError(statusCode,resolvedReason)을 보내고
  • ModelAndView를 반환해
  • WAS에서 다시 오류페이지(/error)를 내부에 요청하는 것이다.
  • 이때, reason파라미터메시지 기능을 사용할 수 있다
  • messages.propertioes를 만들고 거기에 key=value값을 넣는다

다음은 ResponseStatusException이다

  • @ResponseStatus는 개발자가 직접 변경할 수 없는 예외는 적용할 수 없다
  • 추가로 에노테이션을 사용하기 때문에 조건에 따라 동적으로 변경하는 곳도 어렵다
  • 이때 사용하는 예외이다!!!

DefaultHandlerExceptionResolver

  • 만약에 파라미터를 우리가 정한 타입으로 클라이언트가 주지않아 TypeMismatchException이 발생한다면>>
  • 이또한 500오류가 날것이다
  • 그런데 이런 TypeMismatchException는 클아이언트가 요청을 잘못해서 발생하는 문제가 아닐까???
  • 스프링에선 이런 클라이언트가 내는 예외들을 DefaultHandlerExceptionResolver을 이용해 미리 처리해 두었다
  • 이 또한 코드를 보면 sendError()로 서버에 오류를 보내고, 서버에서 다시 해결하는 것을 볼 수 있다
  • 이런 컨트롤러에,,,
  • 문자를 입력해서 ?qqq로 파라미터를 주면?? -> TypeMismatchException발생!
  • 스프링에서 DefaultHandlerException을 이용해 해결한다!!!
  • 400 상태 코드!

지금까지 HTTP 상태 코드를 변경하고, 스프링 내부 예외의 상태코드를 변경하는 기능도 알아보았다. 그런데
HandlerExceptionResolver 를 직접 사용하기는 복잡하다. API 오류 응답의 경우 response 에 직접 데이터를 넣어야 해서 매우 불편하고 번거롭다. ModelAndView 를 반환해야 하는 것도 API에는 잘 맞지 않는다.
스프링은 이 문제를 해결하기 위해 @ExceptionHandler 라는 매우 혁신적인 예외 처리 기능을 제공한다.
그것이 아직 소개하지 않은 ExceptionHandlerExceptionResolver이다

ExceptionHandlerExceptionResolver

API 예외처리의 어려운 점

  • HandlerExceptionResolver 를 떠올려 보면 ModelAndView반환해야 했다. 이것은 API 응답에는 필요하지 않다
  • API 응답을 위해서 HttpServletResponse직접 응답 데이터를 넣어주었다. 이것은 매우 불편하다
  • 특정 컨트롤러에서만 발생하는 예외를 별도로 처리하기 어렵다
    • 예를 들어서 회원을 처리하는 컨트롤러에서 발생하는 RuntimeException 예외와 상품을 관리하는 컨트롤러에서 발생하는 동일한 RuntimeException 예외를 서로 다른 방식으로 처리하고 싶다면 어떻게 해야할까?

@ExceptionHandler

  • 스프링은 API 예외 처리 문제를 해결하기 위해 @ExceptionHandler 라는 애노테이션을 사용하는 매우 편리한 예외
    처리 기능을 제공하는데, 이것이 바로 ExceptionHandlerExceptionResolver 이다.
  • 스프링은 ExceptionHandlerExceptionResolver 를 기본으로 제공하고, 기본으로 제공하는 ExceptionResolver중에 우선순위도 가장 높다. - 실무에서 API 예외 처리는 대부분 이 기능을 사용한다
  • 먼저 예외 발생했을 때, API응답으로 사용할 객체를 정의하자!!!
  • 나중에 규칙을 정해서, 여기에 api로 넣을 객체들을 정해서 json으로 보내주면 된다
@Slf4j
@RestController
public class ApiExceptionV2Controller {
	@ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(IllegalArgumentException.class)
    public ErrorResult illegalExHandler(IllegalArgumentException e){
        log.error("[exceptionHandler] ex",e);
        return new ErrorResult("BAD", e.getMessage());
    }
    @ExceptionHandler
    public ResponseEntity<ErrorResult> userExHandler(UserException e){
        log.error("[exceptionHandler] ex",e);
        ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
        return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);
    }
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler
    public ErrorResult exHandler(Exception e){
        log.error("[exceptionHandler] ex",e);
        return new ErrorResult("EX", "내부 오류");
    }
    ...

}
  • 우리는 API통신의 예외를 위한 코드이다
  • @RestController@Controller와, @ResponseBody기능을 쓴다
  • @ExceptionHandler
    • 에노테이션을 선언하고, 해당 컨트롤러에서만 처리하고 싶은 예외를 지정해준다
    • 해당 컨트롤러에서 예외가 발생하면, 이 메서드가 호출된다
    • 이때 지정한 예외또는 예외의 자식 클래스는 모두 처리할 수 있다

다음은 로직 순서이다!!!

중요!!!

  1. 예외가 터지면 ,ExceptionResolver에서 예외 해결을 시도한다
  2. 그중에 제일 우선순위가 높은 ExceptionHandlerExceptionResolver가 실행된다
  3. ExceptionHandlerExceptionResolver 는 컨트롤러에 @ExceptionHandler가 있는지 찾아본다
  4. 에노테이션이 있으면, ExceptionHandlerExceptionResolver가 , 그 밑에 메서드를 실행해준다
  5. 이 때, ErrorResult를 정상흐름으로 바꿔서, @RestComtroller가 있으므로, json 객체로 바꿔서 반환해준다
  6. 그러면 http상태코드가 200이 되어버린다!!!
  7. 근데 이게 실제로는 오류를 잘 잡았으니까 200이 맞는데, 이건 우리가 원하는게 아니다
  8. 그래서 @ResponseStatus(HttpStatus.~~~)를 붙여서 상태코드를 수정해준다
  9. 그리고 오류가 잘 수정되었기 때문에, 지저분하게 WAS로 올라가지 않는다.
  • 그리고 ExceptionHandler는 컨트롤러안에서만 동작한다, 다른 컨트롤러에서는 동작 하지 않는다.
  • 자세한것이 항상 먼저의 우선순위를 갖는다
  • @ExceptionHandler에서 예외를 생략하면, 파라미터로 받는 예외를 자동으로 지정한다
  • @ExceptionHandler에는 스프링 컨트롤러의 응답처렁 다양한 파라미터와 응답을 지정할 수 있다
    Spring_Exception_Method Arguments
  • 이렇게 ModeAndView를 반환할 수도 있고
  • String으로 반환해 view의 주소로 반환할 수도 있다
  • 매우 유연하다,,,

@ControllerAdvice

  • @ExceptionHandler을 사용해서 예외를 처리할 수 있게 되었다
  • 그런데 정상코드와, 예외 처리 코드가 하나의 컨트롤러안에 있다
  • SRP를 지키기 위해 @ControllerAdvice, @RestControllerAdvice를 사용해 분리할 수 있다
  • 이렇게 하면, 예외를 공통으로 처리할 수 있는데 실무에서는 예외를 공통으로 잘 처리해주는게 매우 중요하다!!!
  • 아까 그 컨트롤러를...

ControllerAdvice와

정상코드로 분리한다

@ControllerAdvice

  • @ControllerAdvice 는 대상으로 지정한 여러 컨트롤러에@ExceptionHandler , @InitBinder 기능을 부여해주는 역할을 한다.
  • @ControllerAdvice 에 대상을 지정하지 않으면 모든 컨트롤러에 적용된다. (글로벌 적용)
  • @RestControllerAdvice 는 @ControllerAdvice 와 같고, @ResponseBody 가 추가되어 있다.
  • @Controller , @RestController 의 차이와 같다.


Spring_Contoller Advice

  • 스프링 공식 문서 예제에서 보는 것 처럼
  1. 특정 애노테이션이 있는 컨트롤러를 지정할 수 있고
  2. 특정 패키지를 직접 지정할 수도 있다.
  3. 패키지 지정의 경우 해당 패키지와 그 하위에 있는 컨트롤러가 대상이 된다 그리고 특정 클래스를 지정할 수도 있다.
  4. 대상 컨트롤러 지정을 생략하면 모든 컨트롤러에 적용된다.
  • @ExceptionHandler@ControllerAdvice조합하면 예외를 깔끔하게 해결할 수 있다.

<정리>

  • 내가 API 요청을 주고 받는 클라이언트와 약속을 한다
  • 우리의 예시를 보자면
  • member에 다음과 같이 들어오면 예외처리를 해주자(500의 서버 예외 말고!!!)
    • ex : RuntimeException
    • bad : IllegalArgumentException
    • user-ex : UserException
    • 등등...
  • ExceptionHandler 즉 @ExceptionHandler를 이용해
  • 만든 에러객체에 넣어서 반환한다
  • 이때, @RestController이므로 JSON객체의 형태로 반환이 된다!
  • 공통인 부분은 Advice로 묶어서
  • 처리해주면 깔끔하게 예외처리를 해줄 수 있다!
profile
개발 공부,정리

0개의 댓글