ㄸxception

정 승 연·2023년 1월 19일
0

목록 보기
1/9

ㄸxception

  • 서블릿이 지원하는 exception 방식
    • Exception
    • response.sendError(HTTP status code, error message)
  • Exception
    • 자바 직접 실행에서 Exception (기본 메커니즘)
      • 자바 메인 메서드 실행시, main 이라는 이름의 스레드 실행, 실행 도중 예외를 잡지 못하고 main 스레드 넘어서 예외가 던져지면, 에외 정보 넘기고 스레드 종료
    • 웹 애플리케이션
      • 사용자 요청별로 별도의 스레드가 할당, 서블릿 컨테이너 안에서 실행

      • try ~ catch 로 예외 잡아서 처리하면 문제 없다. 하지만, 예외를 미처 잡지 못하고 서블릿 밖으로 나가면 WAS까지 전달 가능

        💨 WAS(여기까지 전달) ← filter ← servlet ← interceptor ← controller (예외 발생)
  • 서블릿 예외처리 - 오류페이지 작동 원리
    • 예외가 WAS까지 전달되면 WAS는 오류페이지 정보를 다음코드로 확인한다.
      @Component
      public class WebServerCustomizer implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
          @Override
          public void customize(ConfigurableWebServerFactory factory) {
      
              ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-page/404");
              ErrorPage errorPage500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error-page/500");
      
              ErrorPage errorPageEx = new ErrorPage(RuntimeException.class, "/error-page/500");
      
              factory.addErrorPages(errorPage404, errorPage500, errorPageEx);
          }
      }
    • 확인해보니 RuntimeException의 오류페이지로 ‘/error-page/500이 지정되어있다. WAS는 오류페이지 출력을 위해 지정된 /error-page/500를 다시 요청한다.
    • 오류 페이지 요청 흐름 💨 WAS ‘/error-page/500’ 다시 요청 → filter → servlet → interceptor → controller(/error-page/500) → view
    • 정리하면 다음과 같다.
      **1. 예외가 발생해 WAS까지 전파된다.
    1. WAS는 오류 페이지 경로를 찾아서 내부에서 오류페이지를 호출한다. 이 때 오류 페이지 경로로 필터,서블릿,인터셉터,컨트롤러가 모두 다시 호출된다.**
    • WAS는 오류페이지를 단순히 요청만 하는것이 아니라 오류정보를 request의 attribute에 추가해서 넘겨준다.

filter / interceptor

  • Webconfig
     @Configuration
     public class WebConfig implements WebMvcConfigurer {
     
         @Override
         public void addInterceptors(InterceptorRegistry registry) {
             registry.addInterceptor(new LogInterceptor())
                     .order(1)
                     .addPathPatterns("/**")
                     .**excludePathPatterns**("/css/**", "*.ico", "/error", "/error-page/**"); // 오류 페이지 경로
         }
     
     	//    @Bean
         public FilterRegistrationBean logFilter() {
             FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
             filterRegistrationBean.setFilter(new LogFilter());
             filterRegistrationBean.setOrder(1);
             filterRegistrationBean.addUrlPatterns("/*");
             **filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);**
             // 이 필터는 dispatcherType이 request,error일 때 호출 됩니다.
             return filterRegistrationBean;
         }
     
    • LogFilter
      public class LogFilter implements Filter {
          @Override
          public void init(FilterConfig filterConfig) throws ServletException {
              Filter.super.init(filterConfig);
          }
      
          @Override
          public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
              chain.doFilter(request, response); // 필수 
          }
      
          @Override
          public void destroy() {
              Filter.super.destroy();
          }
      }
    • LogInterceptor : 로그 위치 입력한 interceptor, HandlerInterceptor 인터페이스의 필수 구현체 preHandle, postHandle,afterCompletion은 꼭 오버라이딩 해야함
      public class LogInterceptor implements HandlerInterceptor {
          @Override
          public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
              return HandlerInterceptor.super.preHandle(request, response, handler);
          }
      
          @Override
          public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
              HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
          }
      
          @Override
          public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
              HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
          }
      }

전체 흐름 정리

<aside>
💨 1. WAS(/error-ex, dispatchType=REQUEST) -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러
2. WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)
3. WAS 오류 페이지 확인
4. WAS(/error-page/500, dispatchType=ERROR) -> 필터(x) -> 서블릿 -> 인터셉터(x) ->
컨트롤러(/error-page/500) -> View

</aside>
  • 4번의 필터(x), 인터셉터 (x)는 dispathcerType, excludePathPatterns에 따라 결정
  • ExceptionResolver
    public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
        @Override
        public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
    
            // 정상적인 ModelAndView로 변환
            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) {
                e.printStackTrace();
                log.error("resolver ex", e);
            }
    
            return null;
        }
    }
    💨 1. 컨트롤러에서 예외 발생
    2. 서블릿에 예외 전달 
    3. ExceptionResolver에서 예외 처리 하고 ModelAndView 리턴하여 정상 흐름처럼 보이도록함
    4. WAS 정상 응답
    5. WAS에서 오류 페이지 정보를 다시 찾아 /error 호출
    
    • ExceptionResolver가 ModelAndView 반환하는 이유는, 마치 try-catch 를 하듯이 exception 처리하여 정상 흐름처럼 변경하는 것이 목적이다.
    • response.sendError(SC.BAD_REQUEST,ex.getMesssage()) 를 통해 서블릿 오류 페이지를 찾아 내부 호출, 이후 기본 설정되어있는 /error 호출
    • WebConfig 등록
      <@Override
        public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver>
        resolvers) {
      		resolvers.add(new MyHandlerExceptionResolver());
        }
  • 서블릿 컨테이너까지 예외가 올라가면 복잡하고 지저분하게 프로세스가 실행된다. 반면 ExceptionResolver를 사용하면 예외처리가 상당히 깔끔해진다.
    • 사용자 정의 예외
      public class UserException extends RuntimeException
    • ApiExceptionController 예외 추가
      @ResController
      public class ApiExceptionController{
      
      	@GetMapping("/api/members/{id}")
      	public MemberDto getMember(@PathVariable("id") String id){
      		
      		if(id.equals("user-ex"){
      			throw new UserException("사용자 오류");
      		}
      
      	return new MemberDto(id,"hello"+id);
      
      	@Data
      	@AllArgsConstructor
      	static class MemberDto{
      		private String memberId;
      		private String name;
      	}
      }
    • UserHandlerExceptionResolver 는 너무 복잡하다
      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)) { // accept이 json이면 
                          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{ // 아니면 == html 이면
                          return new ModelAndView("error/500");
                      }
                  }
      
              } catch (IOException e) {
                  log.error("resolver ex", e);
              }
              return null;
          }
      }	
    • ExceptionResolver를 사용하면 컨트롤러에서 예외가 발생해도 Exception에서 예외를 처리한다. 따라서 서블릿 컨테이너까지 예외가 전달되지 않고 스프링 MVC 에서 예외처리가 끝난다. 그러나 직접 ExceptionResolver를 구현하니 복잡하다. 스프링이 제공하는 ExceptionResolver가 있다.
    • ResponseStatusExceptionResolver
      protected ModelAndView applyStatusAndReason(int statusCode, @Nullable String reason, HttpServletResponse response)
      			throws IOException {
      
      		if (!StringUtils.hasLength(reason)) {
      			response.sendError(statusCode);
      		}
      		else {
      			String resolvedReason = (this.messageSource != null ?
      					this.messageSource.getMessage(reason, null, reason, LocaleContextHolder.getLocale()) :
      					reason);
      			**response.sendError(statusCode, resolvedReason);**
      		}
      		return new ModelAndView();
      	}
    • error 메시지 지정
      @GetMapping("/api/response-status-ex1")
          public String responseStatusEx1() {
              throw new BadRequestException();
          }
      
      **@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "error.bad")**
      public class BadRequestException extends RuntimeException {
      }
      
    • ResponseStatusException 사용
      @GetMapping("/api/response-status-ex2")
      public String responseStatusEx2(){
          throw new ResponseStatusException(HttpStatus.NOT_FOUND, "error.bad", 
      					new IllegalArgumentException());
      }
    • DefaultHandlerExceptionResolver

0개의 댓글