스프링 MVC 2편(백엔드 웹 개발 활용 기술)

Ch09. API 예외 처리 - HandlerExceptionResolver

webmaster 2022. 3. 21. 12:50
728x90
  • 상태 코드 변환
    • IllegalArgumentException을 처리하지 못해 밖으로 넘어가는 일이 발생하면 Client에러인 400 에러를 주고 싶다. 어떻게 해야 할까?
@GetMapping("/api/members/{id}")
public MemberDto getMember(@PathVariable("id") String id){
    if(id.equals("ex")){
        throw new RuntimeException("잘못된 사용자");
    }
    if(id.equals("bad")){
        throw new IllegalArgumentException("잘못된 입력 값");
    }
    return new MemberDto(id, "hello " + id);
}
  • id 가 bad로 들어오게 되면 IllegalArgumentException을 호출하도록 한다.
  • 그러나 Server에서 발생하는 에러로 500 에러가 발생하는 것을 확인할 수 있다.

HandlerExceptionResolver

  • 스프링 MVC는 컨트롤러(핸들러) 밖으로 예외가 던져진 경우 예외를 해결하고, 동작을 새로 정의할 수 있는 방법을 제공한다.
  • 컨트롤러 밖으로 던져진 예외를 해결하고, 동작 방식을 변경하고 싶으면 HandlerExceptionResolver를 사용하면 된다. 줄여서 ExceptionResolver 라 한다.

HandlerExceptionResolver 사용 전

사용 전

  • 오류처리가 발생하면 DispatcherServlet에서 postHandler를 호출하지 X, afterCompletion을 호출하고 WAS에 예외를 전달한다.

HandlerExceptionResolver 사용 후

적용 후

  • 예외를 전달받은 DispatcherServlet이 ExceptionResolver를 호출하여 예외 해결을 시도한다.
    • 이때 null을 반환하면 다음 ExceptionResolver를 호출한다.
    • 예외 처리를 해결하고 빈 ModelAndView를 반환하면 Was에 정상적인 응답을 보낸 것처럼 전달해 줄 수 있다.

MyHandlerExceptionResolver 작성하기

@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"); //500에러가 아닌 400에러들 보낸다.
                response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage()); //에러를 여기 서 먹구 400에러로 보낸다,
                return new ModelAndView(); //정상적 흐름으로 리턴이 된다.(예외를 먹어버린다)
            }
        }catch (IOException e){
            log.error("resolver ex", e);
        }

        return null;
    }
}
  • ExceptionResolver 가 ModelAndView를 반환하는 이유는 마치 try, catch를 하듯이, Exception을 처리해서 정상 흐름 처럼 변경하는 것이 목적이다. 이름 그대로 Exception 을 Resolver(해결)하는 것이 목적이다
  • 여기서는 IllegalArgumentException 이 발생하면 response.sendError(400)를 호출해서 HTTP 상태 코드를 400으로 지정하고, 빈 ModelAndView를 반환한다
  • HandlerExceptionResolver의 반환 값에 따른 DispatcherServlet의 동작 방식
    • 빈 ModelAndView
      • new ModelAndView()처럼 빈 ModelAndView를 반환하면 뷰를 렌더링 하지 않고, 정상 흐름으로 서블릿이 리턴된다.
    • ModelAndView 지정
      • ModelAndView에 View , Model 등의 정보를 지정해서 반환하면 뷰를 렌더링 한다.
    • null
      • null을 반환하면, 다음 ExceptionResolver를 찾아서 실행한다. 만약 처리할 수 있는 ExceptionResolver 가 없으면 예외 처리가 안되고, 기존에 발생한 예외를 서블릿 밖으로 던진다
  • 활용
    • 예외 상태 코드 변환
      • 예외를 response.sendError(xxx) 호출로 변경해서 서블릿에서 상태 코드에 따른 오류를 처리하도록 위임
      • 이후 WAS는 서블릿 오류 페이지를 찾아서 내부 호출, 예를 들어서 스프링 부트가 기본으로 설정한 / error 가 호출됨
    • 뷰 템플릿 처리
      • ModelAndView에 값을 채워서 예외에 따른 새로운 오류 화면 뷰 렌더링 해서 고객에게 제공
    • API 응답 처리
      • response.getWriter(). println("hello");처럼 HTTP 응답 바디에 직접 데이터를 넣어주는 것도 가능하다. 여기에 JSON으로 응답하면 API 응답 처리를 할 수 있다

WebConfig 등록

@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
    resolvers.add(new MyHandlerExceptionResolver());
}
  • configureHandlerExceptionResolvers(..)를 사용하면 스프링이 기본으로 등록하는 ExceptionResolver 가 제거되므로 주의, extendHandlerExceptionResolvers를 사용하자

HandlerExceptionResolver 활용

  • 예외가 발생하면 WAS까지 예외가 던져지고, WAS에서 오류 페이지를 정보를 찾아 다시 /error를 호출하는 과정이 너무 복잡하다.
  • ExceptionResolver를 활용하면 예외가 발생했을 때 이런 복잡한 과정 없이 여기에서 문제를 깔끔하게 해결할 수 있다

예외 추가

public class UserException extends RuntimeException{

    public UserException() {
        super();
    }

    public UserException(String message) {
        super(message);
    }

    public UserException(String message, Throwable cause) {
        super(message, cause);
    }

    public UserException(Throwable cause) {
        super(cause);
    }

    protected UserException(String message, Throwable cause, boolean enableSuppression,
        boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

ApiExceptionController 예외 추가

@GetMapping("/api/members/{id}")
public MemberDto getMember(@PathVariable("id") String id){
    if(id.equals("ex")){
        throw new RuntimeException("잘못된 사용자");
    }
    if(id.equals("bad")){
        throw new IllegalArgumentException("잘못된 입력 값");
    }
    if(id.equals("user-ex")){
        throw new UserException("사용자 오류");
    }
    return new MemberDto(id, "hello " + id);
}​

UserHandlerExceptionResolver

@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 요청 해더의 ACCEPT 값이 application/json 이면 JSON으로 오류를 내려주고, 그 외 경우에는 error/500에 있는 HTML 오류 페이지를 보여준다

WebConfig 등록

@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
    resolvers.add(new MyHandlerExceptionResolver());
    resolvers.add(new UserHandlerExceptionResolver());
}
  • ExceptionResolver 를 사용하면 컨트롤러에서 예외가 발생해도 ExceptionResolver에서 예외를 처리해버린다.
  • 따라서 예외가 발생해도 서블릿 컨테이너까지 예외가 전달되지 않고, 스프링 MVC에서 예외 처리는 끝이 난다.
  • 결과적으로 WAS 입장에서는 정상 처리가 된 것이다. 이렇게 예외를 이곳에서 모두 처리할 수 있다는 것이 핵심이다
728x90