Spring 에러/예외처리

선종우·2023년 6월 4일
0

1. 개요

  • 프로그램의 반은 에러와 예외처리라고도 한다. 이번 글에서는 Spring Framework에서 어떻게 예외를 처리할 수 있는지 정리하였다.

2. 서블릿 기본 에러/예외처리 흐름

  • Spring Framework의 예외처리 방법을 정리하기에 앞서 자바와 서블릿의 예외처리 매커니즘을 이해하여야 한다.

  • 자바 프로그램 중 발생한 Exception은 catch문으로 적절한 처리를 하지 않으면 메소드를 호출한 상위 call stack으로 전파되며, main에서도 예외가 처리되지 않으면 쓰레드가 종료된다. 관련글

  • 자바 웹 애플리케이션의 경우 WAS는 사용자 요청마다 쓰레드를 생성한다. 만약 각 사용자 요청(쓰레드) 처리 중 예외가 발생하고 중간에 적절히 처리되지 않으면 예외는 WAS까지 전파된다. WAS는 처리되지 않은 Exception을 Http상태코드와 함께 사용자에게 응답한다.(웹 애플리케이션은 중단되지 않는다.)

  • 자바 웹 애플리케이션은 예외를 발생시키지 않고도 HttpServlet 객체의 sendError()를 이용해서 WAS에 에러정보(Http 상태코드, 메시지)를 전달할 수도 있다.

  • 기존에는 web.xml 등을 이용해 에러/예외에 대한 응답을 처리했다.

  • Spring을 이용할 경우 아래와 같은 방법을 이용해 에러코드에 대한 예외를 WAS에 등록할 수 있다.

@Component
public class WebServerCustomizer implements WebServerCustomizer<ConfigurableWebServerFactory>{
	@Override
    public void customize(ConfigurableWebserverFactory factory){
        //에러 상태코드에 대한 에러 처리 path등록
    	ErrorPage page404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error/404"); 
        //예외 발생(자식 포함)에 대한 에러 처리 path등록
        ErrorPage pageEx = new ErrorPage(EntityNotFoundException, "/error/404");
        //에러 페이지 등록
        factory.addErrorPages(page404, pageEx);
    }
}
  • 서블릿 에러 발생 시 아래와 같은 흐름으로 처리된다.
    1. WAS <- 필터 <- 서블릿(EntityNotFound excepion발생)
    2. WAS(/error/404) 호출) -> 필터 -> 서블릿
    • 에러처리를 위해 내부적으로 request를 생성한 경우 request에 부가적인 정보가 담긴다.(에러/예외 발생 서블릿, 내용, dispatcherType등)
    • 이때 Error인 경우 dispatcherType은 Error로 설정된다.
    • 필터 등록 시 dispatcherType이 일치할 경우만 동작하도록 설정할 수 있다.(에러처리 때문에 불필요한 필터 실행을 막을 수 있음)

3. 스프링 부트의 에러처리

  • Spring boot는 위의 에러페이지 등록 로직을 ErrorMvcAutoConfiguration에 구현해 두었다. 기본 설정을 이용할 경우 에러가 발생하면 /error를 호출하게 되며, 해당 path에는 이미 BasicErrorCtonroller 가 매핑되어 있다.
    -> 개발자는 에러코드에 맞는 에러페이지만 개발하여 적절한 위치에 두면 Spring boot가 에러페이지를 알아서 호출해준다.
    • 자동 설정을 사용하려면 위에 정의하였던 WebserverCustomizer는 사용하면 안 된다.
  • BasicErrorController의 뷰 선택 우선순위
    1. 뷰 템플릿(에러코드와 매칭되는 템플릿 이름)
    resources/templates/error/500.html
    2 정적리소스
    resources/static/error/500.html
    3 default
    resources/templates/error.html
  • 필요한 경우 BasicErrorController를 상속하여 기능을 확장해볼 수 있다.

4. 스프링부트의 예외처리

4.1 ExceptionResolver

  • handler에서 발생한 Exception에 대한 흐름을 바꾸고 싶을 때 사용할 수 있다.
  • handler에서 DispatcherServlet으로 예외로 올라올 경우 ExceptionResolver가 실행되고 ExcetionResolver는 ModelAndView를 반환한다.
public class webConfig implements WebMvcConfigurer{
	@Override
    public void extendHandlerExceptionResolvers(List<HandlerExceptionResolvers> resolvers){
    	resolvers.add(new CustomHandlerExceptionResolver);
        resolvers.add(new CustomApiHandlerExceptionResolver);
    }
}


public class CustomHandlerExceptionResolver implements HandlerExceptionResolver{
	@Oveerride
    public ModelAndView resolveException(
    	HttpServletRequest request,
        HttpServletResponse response,
        Object handler,
        Exception ex){
        try{
          if(ex instanceof EntityNotFoundException){
              response.sendError(HttpServeltResponse.SC_BAD_NOTFOUND, message);
              //View를 지정하지 않았으므로 WAS에서 기본 404에 해당하는 에러페이지로 요청을 보낸다.(테스트 필요, sendError하면서 view를 지정하면?)
              //View를 지정하여 view를 랜더링해 응답할 수도 있다.
              return new ModelAndView();
          }
        }catch(IOException e){
        }
        //null인 경우 다음 resolver를 찾고 최종적으로 처리가 안 된 경우에는 WAS까지 Exception을 전파한다.
        return null; 
        
   }
}

public class CustomApiHandlerExceptionResolver implements HandlerExceptionResolver{
	ObjectMapper objectMapper = new ObjectMapper();
	@Oveerride
    public ModelAndView resolveException(
    	HttpServletRequest request,
        HttpServletResponse response,
        Object handler,
        Exception ex){
        try{
          if(ex instanceof EntityNotFoundException){          
   				String header = request.getHeader("accept");
   				response.sendError(HttpServeltResponse.SC_BAD_NOTFOUND, message);
                if(MediaType.APPLICATION_JSON_VALUE.equals(header){
                Map<String, Object> errorResult = new HashMap<>();
                errorResut.put("ex", ex.getClass());
                String result = objectMapper.writeValueAsString(errorResult);
                response.setContentType();
                response.setCharacterEncoding("utf-8");
                response.getWriter().write(result);
                
                
                }
         }
        }catch(IOException e){
        }
        //null인 경우 다음 resolver를 찾고 최종적으로 처리가 안 된 경우에는 WAS까지 Exception을 전파한다.
        return null; 
        
   }
}
  • 스프링은 기본적으로 1.ExceptionHandlerrExceptionResolver 2. ResponseStatusExceptionResolver 3. DefaultHandlerExceptionResolver를 제공하며, 순서대로 적용된다.
    • ResponseStatusExceptionResolver은 @ResponseStatus가 붙은 Exception또는 ResponseStatusException을 처리해주는 resolver이다.
      -> 예외를 특정 에러 상태코드로 매핑하고 예외가 정상에러처리 흐름으로 바뀌게 된다.
    • DefaultHandlerExceptionResolver은 Spring이 내부에서 발생한 Exception을 적절한 상태코드로 변경하기 위한 resolver이다.
      -> 예상하지 못한 Spring 내부 예외를 자동으로 처리해준다.

4.2 @ExceptionHandler

  • ExceptionHandlerrExceptionResolver를 이용한 예외처리 방법이다.
  • 예외 발생 시 resolver가 예외처리의 흐름을 다시 controller의 ExceptionHandler쪽으로 넘긴다.
@RestController
public controllerex{

	//상태 지정 안 해주면 200으로 응답해줌
    @ResponseStatus(HttpStatus.NOT_FOUND)
    @ExceptionHanler(EntityNotFoundException.class)
    public ErrorResult EntityNotFoundExHandler(EntityNotFoundException e){
    	return enw ErrorResult("Not Found", e.getmessage());
    }
	@GetMapping("/members/{id}")
    public MemberResponse getMember(@PathValirable Long id){
    }
}

4.3 @ControllerAdvice

  • @ExceptionHandler를 컨트롤러 내부에 선언하게 되면 에러처리 로직이 controller와 섞이게 되고, 선언된 컨트롤러 내부에서만 처리가 가능한 문제점이 있다. 이를 해결할 수 있는 방법이 @ControllerAdvice이다.
    @RestContollerAdvice
    public class GlobalAdvice{
    @ExceptionHanler(EntityNotFoundException.class)
       public ErrorResult EntityNotFoundExHandler(EntityNotFoundException e){
       	return enw ErrorResult("Not Found", e.getmessage());
       }
    }

0개의 댓글