[spring 예외 처리] 02.프로그래밍을 통한 예외 처리 1
- -
설정을 통해서 예외를 처리하는 방법은 간단하지만 개입할 여지가 없어진다. 예를 들어 오류 상황이 발생했을 때 logging을 한다든가, 관리자에게 메일을 보낸다든가 하는 일을 처리하기는 어렵다.
이처럼 예외 처리 시 부가적인 작업을 처리해야 하는 경우는 프로그래밍적으로 처리해야 한다.
이번 포스트에서는 Spring에서 프로그래밍적으로 예외를 처리하는 방식에 대해 알아보자.
개별 @Controller에서의 예외 처리
@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의 또다른 사용법으로 사용자정의 예외 클래스를 만들고 해당 클래스에 @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
@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의 활용 예
예외 처리와는 상관 없지만 말이 나온김에 간단히 @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));
}
}
}
@ExceptionHandler를 활용한 예외 처리
@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의 한정
@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 {}; // 애너테이션 기준으로 한정
}
@Order를 이용한 우선 순위 지정
가끔 @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로 전달된다.
'Spring MVC > 03.예외 처리' 카테고리의 다른 글
[spring 예외 처리] 04. 프로그래밍적인 404 처리 (0) | 2022.11.07 |
---|---|
[spring 예외 처리] 03.프로그래밍을 통한 예외 처리 2 (0) | 2022.11.06 |
[spring 예외 처리] 01.설정을 통한 예외 처리 (4) | 2021.10.22 |
소중한 공감 감사합니다