본문 바로가기
Spring

[Spring] 스프링이 제공하는 HandlerExceptionResolver에 대해서(ExceptionHandlerExceptionResolver)

by 개미가되고싶은사람 2024. 7. 4.

목차

    HandlerExceptionResolver 기본 개념

    예외 처리를 하지 못해서 예외가 컨트롤러 밖으로 전달된 경우 이 예외를 해결하고, 동작을 새로 정의할 수 있는 방법을 제공한다. 즉 컨트롤러 밖으로 던져진 예외를 해결하고, 동작 방식을 변경하고 싶으면 HandlerExceptionResolver를 사용하면 됩니다.

     

     

     

     

    ExceptionResolver 적용 후

    위에 사진을 보면 컨트롤러에 가는 중 예외가 발생한다. 예외 발생 시 HandlerExceptionResolver에게 물어봅니다. 이 오류 해결할 수 있어? 어 나 해결할 수 있어 그러면 HandlerExceptionResolver를 구현한 클래스에서 sendError()메소드를 사용하면 서버는 sendError에 있는 HTTP 상태 코드를 반환하게 됩니다. 그리고 ModelAndView를 반환하는 이유는 예외를 처리해서 정상 흐름처럼 변경하기 위해 ModelAndView를 반환합니다.

     

     

    HandlerExceptionResolver 구현체 예시

    @Slf4j
    public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
        @Override
        public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response,
                                             Object handler, Exception ex) {
            try {
                if (ex instanceof IllegalAccessException) {
                    log.info("IllegalAccessException to 400");
                    response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
                    return new ModelAndView();//
                }
            } catch (IOException e) {
                log.error("resolver ex", e);
            }
            return null;
        }
    }
    @Configuration
    public class WebConfig implements WebMvcConfigurer {
        @Override // MyHandlerExceptionResolver 등록
        public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
            resolvers.add(new MyHandlerExceptionResolver());
        }
    }

    ❗주의할 점❗

    WebMvcConfigurer인터페이스에 configureHandlerExceptionResolvers()메소드를 사용하면 스프링이 기본으로 등록하는 ExceptionResolver이 제거가 됩니다.

     

     

    HandlerExceptionResolver 반환 값에 따른 비교

    - 빈 ModelAndView, ModelAndView 지정, null 값 반환 총 3가지로 분류할 수 있습니다.

    1. 빈 ModelAndView: 뷰를 렌더링 하지 않고, 정상 흐름으로 서블릿이 리턴된다.
    2. ModelAndView 지정: ModelAndView에 View , Model 등의 정보를 지정해서 반환하면 뷰를 렌더링 한다. - 오류 페이지로 사용할 수 있다
    3. null:다른 ExceptionResolver 구현체를 찾아서 실행한다. 만약 처리할 수 있는 ExceptionResolver가 없으면 예외 처리가 안되고, 기존에 발생한 예외를 서블릿 밖으로 던집니다.

     

     

    ExceptionResolver를 사용하면 컨트롤러에서 예외가 발생해도 예외를 처리하기 위해 만든 xxxxxExceptionResolver에서 예외를 처리해버립니다. 이 과정을 단축할 수 있는 방법에 대해서 알려드리겠습니다.!!!  

     

     

    HandlerExceptionResolver 활용

    1. xxxxException를 상속 받는 구현체 예시

    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);
        }
    }

     

    2. 예외를 처리하는 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/400");
                    }
                }
            } catch (IOException e) {
                log.error("resolver ex", e);
            }
            return null;
        }
    }

    해당 코드가 핵심 로직인데  UserException라는 예외가 오면 if문 아래에서 예외를 처리합니다.

     

    3. UserHandlerExceptionResolver을 등록

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

     

     

    4. 컨트롤러에서 xxxxException이 발생하는 경우에 xxxxException로 예외 처리

    @Slf4j
    @RestController
    public class ApiExceptionController {
        @Data
        @AllArgsConstructor
        static class MemberDTO {
            private String MemberId;
            private String name;
        }
    
        @GetMapping("/api/members/{id}")
        public MemberDTO getMember(@PathVariable("id") String id) {
            if (id.equals("ex")) {
                throw new RuntimeException("잘못된 사용자");
            } (id.equals("user-ex")) {
                throw new UserException("사용자 오류");
            }
    
            return new MemberDTO(id, "hello" + id);
        }
    
    }

    이렇게 로직을 구성하면 컨트롤러에서 예외가 발생해도 예외를 처리하기 위해 만든 xxxxxExceptionResolver에서 예외를 처리해버린다. 따라서 예외가 발생해도 서블릿 컨테이너까지 예외가 전달되지 않고, 컨트롤러에서 예외 처리를 처리하는 효과를 볼 수 있습니다.(정확히는 스프링 MVC에서 처리함) 결과적으로 WAS 입장에서는 정상 처리 및 예외가 발생한 지 모른다는 것이 핵심입니다.