잘못된 요청
, 서버 내부의 에러
등 여러 원인으로 예외 상황을 맞딱뜨리게 된다.보여줄 HTML 페이지
나 응답할 JSON 객체
에 대해 설정해야한다.server.error.whitelabel.enabled
설정에 따라, 스프링부트나 서블릿이 기본적으로 제공하는 예외 페이지가 나타난다.@Component
public class WebServerCustomizer implements
WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
@Override
public void customize(ConfigurableWebServerFactory factory) {
// 404 응답 코드 발생 시 > 내부 요청 발생
ErrorPage errorPage1= new ErrorPage(HttpStatus.NOT_FOUND, "에러페이지 요청 url");
// 런타임 예외 발생 시 > 내부 요청 발생
ErrorPage errorPage2= new ErrorPage(RuntimeException.class, "에러페이지 요청 url");
// 에러 페이지 등록
factory.addErrorPages(errorPage404, errorPage500, errorPageEx);
}
}
클라이언트 요청
→WAS
→서블릿 필터
→디스패쳐 서블릿
→인터셉터
→컨트롤러
→예외 발생
→인터셉터
→디스패쳐 서블릿
→서블릿 필터
→WAS
→예외 감지 후 ErrorPage 등록 여부에 따라 내부 요청
→서블릿 필터
→디스패쳐 서블릿
→인터셉터
→컨트롤러
→예외 발생
→인터셉터
→디스패쳐 서블릿
→View 렌더링
public enum DispatcherType {
/**
* {@link RequestDispatcher#forward(ServletRequest, ServletResponse)}
*/
FORWARD,
/**
* {@link RequestDispatcher#include(ServletRequest, ServletResponse)}
*/
INCLUDE,
/**
* Normal (non-dispatched) requests.
*/
REQUEST,
/**
* {@link AsyncContext#dispatch()}, {@link AsyncContext#dispatch(String)}
* and
* {@link AsyncContext#addListener(AsyncListener, ServletRequest, ServletResponse)}
*/
ASYNC,
/**
* When the container has passed processing to the error handler mechanism
* such as a defined error page.
*/
ERROR
}
DispatcherType
은 ERROR
가 된다.FilterRegistrationBean
설정에서 .setDispatcherTypes(DispatcherType.REQUEST)
로 주면 DispatcherType.ERROR
일 때는 필터를 거치지 않게 된다.예외 요청 시 인터셉터
- 인터셉터는 DispatcherType 여부와 상관 없이 경로 패턴에 따라 호출된다.
- 따라서 인터셉터에는
.excludePathPatterns()
설정을 통하여 예외 요청을 제거해주면 된다.
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
private final ErrorProperties errorProperties;
//...
// 헤더의 미디어 타입이 text/html일 때 호출되는 컨트롤러 메소드
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
// 헤더의 미디어 타입이 text/html이 아닐 경우 호출되는 컨트롤러 메소드
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
// ...
protected ErrorAttributeOptions getErrorAttributeOptions(HttpServletRequest request, MediaType mediaType) {
ErrorAttributeOptions options = ErrorAttributeOptions.defaults();
if (this.errorProperties.isIncludeException()) {
options = options.including(Include.EXCEPTION);
}
if (isIncludeStackTrace(request, mediaType)) {
options = options.including(Include.STACK_TRACE);
}
if (isIncludeMessage(request, mediaType)) {
options = options.including(Include.MESSAGE);
}
if (isIncludeBindingErrors(request, mediaType)) {
options = options.including(Include.BINDING_ERRORS);
}
return options;
}
}
errorHtml()
의 경우 기본적으로 뷰 페이지 정보를 아래 순서로 탐색한다.500.html
or 400.html
or 404.html
or ...5xx.html
or 4xx.html
or ...500.html
or 400.html
or 404.html
or ...5xx.html
or 4xx.html
or ...BasicController.errorHtml()
로 내부 요청이 일어나서 해당 뷰페이지를 렌더링하여 클라이언트에게 보여준다.text/html
아닌 경우에 예외 발생 시, BasicController.error()
로 내부 요청이 일어나서 ResponseEntity<>
에 원하는 Map으로 응답받이에 JSON 형태로 담겨 응답한다.# exception 정보 포함 여부
server.error.include-exception=true
# 예외 메세지 포함 여부
server.error.include-message=false
# trace 포함 여부
server.error.include-stacktrace=false
# errors 내용 포함 여부
server.error.include-binding-errors=false
errorHtml()
의 모델 어트리뷰트 내용이나, error()
의 ResponseEntity 내용이 달라진다.BasicController
에서 처리하는 내용과는 다르게 전달하는 내용이 충분하지 않거나, 너무 많은 정보를 넘겨주게 될 수 있다.재요청
하게 된다.이러한 한계점 때문에, Spring 에서는 대부분
HandlerExceptionResovler
를 사용하여 예외 처리를 한다.
public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
return null;
}
}
HandlerExceptionResolver
를 구현하는 클래스를 만들고 resolveException()
를 재정의하므로서 HandlerExceptionResovler를 구현할 수 있다.@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(new MyHandlerExceptionResolver());
resolvers.add(...);
}
@Override
public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
// 이 메소드는 Spring이 기본적으로 HandlerExceptionResolver를 무시하게 된다. (사용하지 않는 편이 좋겠따.)
}
}
WebMvcConfigurer
에 등록해주면 된다.요청
,응답
,핸들러
,예외
정보를 갖고 호출된다.ModelAndView
를 리턴한다.ModelAndView
는 거의 Controller와 마찬가지로 동작한다.빈 ModelAndView
를 반환한다.return null
을 하면 해당 resovler는 통과하고 다음 HandlerExceptionResolver를 탐색하고 끝내 해결이 되지 않은 에러는 그냥 WAS로 던져진다.resolveException()
을 구현해서 사용하게 되면, 반환이 ModelAndView로 고정이 되므로 응답 바디에 직접 내용을 써야하는 JSON 같은 경우에는 번거로움이 있다.public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
if(ex instanceof RuntimeException) {
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();
}
// 500 예외 뷰페이지 렌더링하여 응답
return new ModelAndView("error/500");
}
//다음 리졸버로 넘김
return null;
}
}
Spring 에서 기본적으로 제공하는 HandlerExceptionResolver 구현체가 매우 잘 되어 있다.
그를 사용하여 예외 처리
@ExceptionHanlder
어노테이션을 처리 (⚡️제일 중요)@ExceptionHandler
어노테이션이 있는 메소드를 통하여 예외 해결을 시도한다.@RestController
public class MyController {
//@ExceptionHandler
//@ExceptionHandler(Exception.class)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResultDTO illegalExeptionHandler(IllegalArgumentException e) {
return new ErrorResultDTO(...);
}
@GetMapping("/test")
public ResultDTO test() {
if(예외 발생 시나리오) {
throw new IllegalArgumentException();
}
return new ResultDTO(...);
}
}
@ExceptionHandler
에 속성으로 있는 예외(그 자식 예외)에 대해서 해당 처리를 해준다.해결
하고 응답하는 것이므로 상태코드가 200
이므로 상태코드를 4xx or 500 으로 적절히 넘겨줄 필요가 있다.@ResponseStatus(HttpStatus.BAD_REQUEST)
ResponseEntity(new ErrorResultDTO(), HttpStatus.BAD_REQUEST)
로 반환ExceptionHandlerExceptionResolver
적용을 위해서 매 핸들러 마다 @ExceptionHandler
메소드를 넣어야 적용이 가능하다.@ControllerAdvice
는 컨트롤러들을 지정하여 @ExceptionHandler
메소드를 지정할 수 있다.가장 많이 쓸 형태
//@ControllerAdvice(annotations = RestController.class)
//@ControllerAdvice("org.test")
//@ControllerAdvice(assignableTypes = {MyController.class, ...})
@RestControllerAdvice(annotations = RestController.class)
public class ExceptionControllerAdvice {
//@ExceptionHandler
//@ExceptionHandler(Exception.class)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResultDTO illegalExeptionHandler(IllegalArgumentException e) {
return new ErrorResultDTO(...);
}
}
@RestController
public class MyController {
@GetMapping("/test")
public ResultDTO test() {
if(예외 발생 시나리오) {
throw new IllegalArgumentException();
}
return new ResultDTO(...);
}
}
@ResponseStatus(value = HttpStatus.NOT_FOUND, reason)
를 지정한 예외를 인지하여 예외 결과 반환