ExceptionHandler의 value 속성은 처리가능한 예외의 배열을 설정한다. 또한 @ExceptionHandler가 선언된 메서드의 파라미터로는 request mapping 메서드에서 사용되는 파라미터들(Model, HttpServletRequest 등)을 사용할 수 있다.
만약 @Controller의 request mapping 메서드에서 예외가 던져지면 해당 예외를 처리 가능한 @ExceptionHandler 메서드가 예외를 처리한다. 이때 상속 관계에 의해 하위 예외는 상위 예외들이 처리해 준다.
다음의 예는 @Controller에서 발생한 모든 예외를 처리해주는 @ExceptionHandler의 사용 예이다.
@Controller@Slf4jpublicclassHomeController{
@GetMapping("/problem1")public String problem1(){
inti=1 / 0; // ArithmeticException 발생return"index";
}
@ExceptionHandler(Exception.class)@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)public Object handleException(Exception e, HttpServletRequest request, Model model){
// 필요한 예외 정보 구성
model.addAttribute("time", Calendar.getInstance().getTime());
model.addAttribute("url", request.getServletPath());
model.addAttribute("message", e.getMessage());
model.addAttribute("status", HttpStatus.INTERNAL_SERVER_ERROR);
// 일반 HTTP 요청인지 XMLHttp 요청인지에 따라 Stringaccept= request.getHeader("accept");
if (accept.contains(MediaType.TEXT_HTML_VALUE)) {
return"/error/500";
} else {
returnnewResponseEntity<Model>(model, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
일반 http 요청과 xmlhttp 요청에 따라 에러 페이지 또는 json 으로 결과를 반환하기 위해서 request header의 accept 속성을 이용하는 점도 눈여겨두자.
이 방법을 사용하면 @Controller에서 발생한 예외가 @ExceptionHandler에서 소멸되기 때문에 WAS에는 예외가 전파되지 않는다. 물론 @ExceptionHandler가 처리하지 못한 예외가 있다면 그 예외는 WAS로 던져져서 WAS의 예외 처리 방침을 따른다.
사용자 정의 예외 클래스에 @ResponseStatus 활용
@ResponseStatus의 또다른 사용법으로 사용자정의 예외 클래스를 만들고 해당 클래스에 @ResponseStatus를 설정할 수도 있다.
즉 예외별로 에러 코드를 설정할 수 있는데 단순히 상황을 전파하는 것일 뿐 특별히 예외를 처리하거나 하지는 않는다. 이 예외가 발생했을 때 동작할 @ExceptionHandler 또는 WAS가 최종적으로 처리해주어야 한다. 자주 사용되지는 않는다.(개인적으로는 안쓴다? ㅎ)
전역의 예외 처리
@Controller에 설정한 @ExceptionHandler는 해당 @Controller에서 발생한 예외에 대해서만 처리한다. 당연히 @Controller가 많아지면 그만큼의 코딩이 필요하다. 이럴 경우 사용할 수 있는 것이 @ControllerAdvice이다.
@ControllerAdvice
@ControllerAdvice는 클래스 레벨에 선언하는 애너테이션으로 @Component의 한 종류이기 때문에 빈으로 등록해서 사용한다.
@ControllerAdvice는 이름 그대로 전역으로 @Controller에게 조언해줄 수 있는 녀석으로 애너테이션을 이용해 크게 3가지 도움을 줄 수 있다.
@ModelAttribute : 모든 @Controller에 대해 기본적으로 가져갈 data를 추가할 수 있다.
@InitBinder: 모든 @Controller에 대한 data binding을 설정할 수 있다.
@ExceptionHandler: 모든 @Controller에서 발생하는 예외를 통합 처리할 수 있다.
@ModelAttribute와 @InitBinder의 활용 예
예외 처리와는 상관 없지만 말이 나온김에 간단히 @ModelAttribute와 @InitBinder의 활용예를 살펴보자
@ControllerAdvicepublicclassGlobalControllerAdvice{
// 모든 요청에 대해 map이라는 이름으로 데이터를 Model에 추가한다.@ModelAttributepublic Map<String, Object> getMainMenu(){
Map<String, Object> menu = newHashMap<>();
menu.put("admin", "/admin");
menu.put("login", "/login");
return menu;
}
// 모든 날짜 형태의 데이터를 yyyy-MM-dd(E)의 포멧으로 처리한다.@InitBinderpublicvoidcommonBinding(WebDataBinder binder){
SimpleDateFormatdateFormat=newSimpleDateFormat("yyyy-MM-dd (E)");
binder.registerCustomEditor(Date.class, newCustomDateEditor(dateFormat, true));
if (binder.getTarget() instanceof UserInfo) {
DecimalFormatdoubleFormat=newDecimalFormat("$##,###");
binder.registerCustomEditor(Integer.class, "salary",
newCustomNumberEditor(Integer.class, doubleFormat, true));
}
}
}
@ExceptionHandler를 활용한 예외 처리
@ControllerAdvice에 선언한 @ExceptionHandler 메서드는 앞서 이야기 했듯이 모든 @Controller에서 발생한 예외를 처리할 수 있다. 사용법은 @Controller 클래스에 표현했을 때와 동일하다.
@ControllerAdvice@Slf4jpublicclassGlobalControllerAdvice{
@ExceptionHandler(RuntimeException.class)public Object handleException(Exception e, HttpServletRequest request, Model model){
model.addAttribute("time", Calendar.getInstance().getTime());
model.addAttribute("url", request.getServletPath());
model.addAttribute("message", e.getMessage());
model.addAttribute("status", HttpStatus.INTERNAL_SERVER_ERROR);
Stringaccept= request.getHeader("accept");
if (accept.contains(MediaType.TEXT_HTML_VALUE)) {
return"/error/500";
} else {
returnnewResponseEntity<Map<String, Object>>(newHashMap<String, Object>(),
HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@ExceptionHandler(ArithmeticException.class)public Object arithmeticExceptionHandling(ArithmeticException e,
HttpServletRequest request, Model model){
// do somethingreturnnull;
}
}
위의 예에서 살펴보면 만약 Controller에서 ArithmethcException이 발생하면 arithmeticExceptionHandling 메서드가 처리하고 나머지 RuntimeException 계열의 예외가 발생하면 handleException이 처리한다.
@ControllerAdvice의 한정
@ControllerAdvice의 기본 scope는 전역으로 모든 @Controller에 적용된다. 필요에 따라서 package, annotation, type 기반으로 적용 대상을 한정할 수 있다.
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Componentpublic@interface ControllerAdvice {
@AliasFor("basePackages")
String[] value() default {};
@AliasFor("value")
String[] basePackages() default {}; // 패키지 기준으로 한정
Class<?>[] basePackageClasses() default {}; // 클래스가 소속된 패키지 기준으로 한정
Class<?>[] assignableTypes() default {}; // 클래스 기준으로 한정
Class<? extendsAnnotation>[] annotations() default {}; // 애너테이션 기준으로 한정
}
@Order를 이용한 우선 순위 지정
가끔 @ControllerAdvice가 여러 개인 경우는 우선순위를 지정해야하는 경우가 있는데 이를 위해 @Order 애너테이션을 사용할 수 있다.
@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})@Documentedpublic@interface Order {
intvalue()default Ordered.LOWEST_PRECEDENCE;
}
@Ordered의 value는 int 값으로 값이 낮을 수록 우선순위가 높다. 즉 Ordered에 선언된 상수인 LOWEST_PRECEDENCE의 실제 값은 Integer.MAX_VALUE이고 HIGHEST_PRECEDENCE는 Integer.MIN_VALUE이다.
예외처리의 우선순위
만약 @Controller에도 @ExceptionHandler가 선언되어있고 @ControllerAdvice에도 @ExceptionHandler가 선언되어있다면 누가 우선권을 갖게 될까?
일반적으로 프로그래밍에서는 세부적으로 설정한 것이 더 강한 우선순위를 갖게 된다. 스프링의 예외처리도 마찬가지로 @Controller에 선언된 @ExceptionHandler가 먼저 처리되고 거기서 처리하지 못하면 @ControllerAdvice에 선언된 @ExceptionHandler를 찾는다. 여기서도 처리하지 못하면 예외는 WAS로 전달된다.