Spring MVC/03.예외 처리

[spring 예외 처리] 02.프로그래밍을 통한 예외 처리 1

  • -

설정을 통해서 예외를 처리하는 방법은 간단하지만 개입할 여지가 없어진다. 예를 들어 오류 상황이 발생했을 때 logging을 한다든가, 관리자에게 메일을 보낸다든가 하는 일을 처리하기는 어렵다.

이처럼 예외 처리 시 부가적인 작업을 처리해야 하는 경우는 프로그래밍적으로 처리해야 한다.

이번 포스트에서는 Spring에서 프로그래밍적으로  예외를 처리하는 방식에 대해 알아보자.

 

 

@Controller에서 예외를 처리하는 가장 기본적인 방법은 @ExceptionHandler를 활용하는 것이다.

@Target(ElementType.METHOD) public @interface ExceptionHandler { Class<? extends Throwable>[] value() default {}; }

ExceptionHandler의 value 속성은 처리가능한 예외의 배열을 설정한다. 또한 @ExceptionHandler가 선언된 메서드의 파라미터로는 request mapping 메서드에서 사용되는 파라미터들(Model, HttpServletRequest 등)을 사용할 수 있다.

만약 @Controller의 request mapping 메서드에서 예외가 던져지면 해당 예외를 처리 가능한 @ExceptionHandler 메서드가 예외를 처리한다. 이때 상속 관계에 의해 하위 예외는 상위 예외들이 처리해 준다.

추가로 응답을 보낼 때 @ResponseStatus를 통해 상태 코드를 설정할 수 있다.

@Target({ElementType.TYPE, ElementType.METHOD}) public @interface ResponseStatus { HttpStatus value() default HttpStatus.INTERNAL_SERVER_ERROR; String reason() default ""; }

 

다음의 예는 @Controller에서 발생한 모든 예외를 처리해주는 @ExceptionHandler의 사용 예이다.

@Controller @Slf4j public class HomeController { @GetMapping("/problem1") public String problem1() { int i = 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 요청인지에 따라 String accept = request.getHeader("accept"); if (accept.contains(MediaType.TEXT_HTML_VALUE)) { return "/error/500"; } else { return new ResponseEntity<Model>(model, HttpStatus.INTERNAL_SERVER_ERROR); } } }

일반 http 요청과 xmlhttp 요청에 따라 에러 페이지 또는 json 으로 결과를 반환하기 위해서 request header의 accept 속성을 이용하는 점도 눈여겨두자.

이 방법을 사용하면 @Controller에서 발생한 예외가 @ExceptionHandler에서 소멸되기 때문에 WAS에는 예외가 전파되지 않는다.  물론 @ExceptionHandler가 처리하지 못한 예외가 있다면 그 예외는 WAS로 던져져서 WAS의 예외 처리 방침을 따른다.

 

 

@ResponseStatus의 또다른 사용법으로 사용자정의 예외 클래스를 만들고 해당 클래스에 @ResponseStatus를 설정할 수도 있다.

@GetMapping("/notYetReady") public String notYetReady(Model model, String lang) { logger.trace("lang: {}", lang); if (lang == null) { throw new NotYetReadyException(); } model.addAttribute("message", lang); return "showMessage"; } @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public class NotYetReadyException extends RuntimeException{ public NotYetReadyException() { super("아직 구현되지 않은 기능입니다."); } }

즉 예외별로 에러 코드를 설정할 수 있는데 단순히 상황을 전파하는 것일 뿐 특별히 예외를 처리하거나 하지는 않는다. 이 예외가 발생했을 때 동작할 @ExceptionHandler 또는 WAS가 최종적으로 처리해주어야 한다. 자주 사용되지는 않는다.(개인적으로는 안쓴다? ㅎ)

 

 

@Controller에 설정한 @ExceptionHandler는 해당 @Controller에서 발생한 예외에 대해서만 처리한다. 당연히 @Controller가 많아지면 그만큼의 코딩이 필요하다. 이럴 경우 사용할 수 있는 것이 @ControllerAdvice이다.

 

@ControllerAdvice는 클래스 레벨에 선언하는 애너테이션으로 @Component의 한 종류이기 때문에 빈으로 등록해서 사용한다.

@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Component public @interface ControllerAdvice {}

 

@ControllerAdvice는 이름 그대로 전역으로 @Controller에게 조언해줄 수 있는 녀석으로 애너테이션을 이용해 크게 3가지 도움을 줄 수 있다.

  • @ModelAttribute : 모든 @Controller에 대해 기본적으로 가져갈 data를 추가할 수 있다.
  • @InitBinder: 모든 @Controller에 대한 data binding을 설정할 수 있다.
  • @ExceptionHandler: 모든 @Controller에서 발생하는 예외를 통합 처리할 수 있다.

 

예외 처리와는 상관 없지만 말이 나온김에 간단히 @ModelAttribute와 @InitBinder의 활용예를 살펴보자

@ControllerAdvice public class GlobalControllerAdvice { // 모든 요청에 대해 map이라는 이름으로 데이터를 Model에 추가한다. @ModelAttribute public Map<String, Object> getMainMenu() { Map<String, Object> menu = new HashMap<>(); menu.put("admin", "/admin"); menu.put("login", "/login"); return menu; } // 모든 날짜 형태의 데이터를 yyyy-MM-dd(E)의 포멧으로 처리한다. @InitBinder public void commonBinding(WebDataBinder binder) { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd (E)"); binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true)); if (binder.getTarget() instanceof UserInfo) { DecimalFormat doubleFormat = new DecimalFormat("$##,###"); binder.registerCustomEditor(Integer.class, "salary", new CustomNumberEditor(Integer.class, doubleFormat, true)); } } }

 

@ControllerAdvice에 선언한 @ExceptionHandler 메서드는 앞서 이야기 했듯이 모든 @Controller에서 발생한 예외를 처리할 수 있다. 사용법은 @Controller 클래스에 표현했을 때와 동일하다.

@ControllerAdvice @Slf4j public class GlobalControllerAdvice { @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); String accept = request.getHeader("accept"); if (accept.contains(MediaType.TEXT_HTML_VALUE)) { return "/error/500"; } else { return new ResponseEntity<Map<String, Object>>(new HashMap<String, Object>(), HttpStatus.INTERNAL_SERVER_ERROR); } } @ExceptionHandler(ArithmeticException.class) public Object arithmeticExceptionHandling(ArithmeticException e, HttpServletRequest request, Model model) { // do something return null; } }

위의 예에서 살펴보면 만약 Controller에서 ArithmethcException이 발생하면 arithmeticExceptionHandling 메서드가 처리하고 나머지 RuntimeException 계열의 예외가 발생하면 handleException이 처리한다.

 

@ControllerAdvice의 기본 scope는 전역으로 모든 @Controller에 적용된다. 필요에 따라서 package, annotation, type 기반으로 적용 대상을 한정할 수 있다.

@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Component public @interface ControllerAdvice { @AliasFor("basePackages") String[] value() default {}; @AliasFor("value") String[] basePackages() default {}; // 패키지 기준으로 한정 Class<?>[] basePackageClasses() default {}; // 클래스가 소속된 패키지 기준으로 한정 Class<?>[] assignableTypes() default {}; // 클래스 기준으로 한정 Class<? extends Annotation>[] annotations() default {}; // 애너테이션 기준으로 한정 }

 

가끔 @ControllerAdvice가 여러 개인 경우는 우선순위를 지정해야하는 경우가 있는데 이를 위해 @Order 애너테이션을 사용할 수 있다. 

@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD}) @Documented public @interface Order { int value() 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로 전달된다.

 

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.