
잘못된 요청, 서버 내부의 에러 등 여러 원인으로 예외 상황을 맞딱뜨리게 된다.보여줄 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)를 지정한 예외를 인지하여 예외 결과 반환