Spring MVC/03.예외 처리

[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로 전달된다.

 

반응형
Contents

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

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