예외 핸들링

스프링부트 웹 MVC의 exception handling에 대해 살펴보자.

스프링부트에서는 앱을 실행하면 기본 에러 핸들러가 이미 등록되어 있으며, 예외가 발생할 경우 해당 핸들러가 메시지를 응답한다.

HTML 응답

JSON 응답

위 이미지처럼 curl 요청을 보내면 json 응답을 받는다.

BasicErrorController

기본 핸들러의 로직은 BasicErrorController에 들어있다. 이 클래스를 살펴보자. BasicErrorController는 HTTP와 JSON 응답을 지원한다.

@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, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
    response.setStatus(status.value());
    ModelAndView modelAndView = resolveErrorView(request, response, status, model);
    return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}

accept 헤더로 "text/html"을 요청한 경우, errorHtml 메소드에서 html을 만들어서 ModelAndView를 반환한다.

@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
    Map<String, Object> body = getErrorAttributes(request,
                                                  isIncludeStackTrace(request, MediaType.ALL));
    HttpStatus status = getStatus(request);
    return new ResponseEntity<>(body, status);
}

accept 헤더 값이 "text/html" 가 아닌 경우 error 메소드에서 ResponseEntity를 반환하여 json 데이터를 응답한다.

ExceptionHandler

ExceptionHandler는 스프링 MVC에서 예외를 처리하는 방법 중 하나로 메소드에 지정하는 어노테이션이다. "/hello" 요청을 매핑한 컨트롤러에서 예외가 발생하는 상황을 가정해보자.

@Controller
public class SampleController {

    @GetMapping("/hello")
    public String hello() {
        throw new SampleException();
    }

    @ExceptionHandler(SampleException.class)
    public @ResponseBody AppError sampleError(SampleException e) {
        AppError appError = new AppError();
        appError.setMessage("error.app.key");
        appError.setReason("IDK IDK IDK");
        return appError;
    }

}

public class AppError {

    private String message;
    private String reason;

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public String getReason() {
        return reason;
    }

    public void setReason(String reason) {
        this.reason = reason;
    }
}

public class SampleException extends RuntimeException {

}

SampleException 예외를 처리하기 위한 메소드에 @ExceptionHandler를 지정한다. ExceptionHandler가 지정된 메소드에서 AppError라는 커스텀 에러정보 클래스를 응답한다.

앱을 실행하고, "/hello"를 요청하면 {"message":"error.app.key","reason":"IDK IDK IDK"} 와 같은 json 데이터가 응답되는 것을 알 수 있다.

ControllerAdvice

@ExceptionHandler는 자신이 위치한 컨트롤러에서 발생한 예외만 처리한다. 전역에서 모든 예외를 처리하기 위해서는 별도의 클래스를 생성하고 @ControllerAdvice를 지정해야 한다.

@ControllerAdvice가 지정된 클래스 내부에서 메소드에 @ExceptionHandler를 지정하여 각 예외별로 처리할 메소드를 구현한다.

BasicErrorController의 예외 처리 방법

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}") // 1
public class BasicErrorController extends AbstractErrorController {
    // ...
}

다시 BasicErrorController로 돌아가보자. BasicErrorController는 기본으로 설정되는 전역 에러 핸들러이다. 그러나, @ControllerAdvice와 @ExceptionHandler를 찾아볼 수가 없다. 그렇다면 어떻게 전역으로 예외를 처리하는 것일까?

  • ${server.error.path:${error.path:/error}}
    • ${error.path:/error}
      • error.path 프로퍼티가 정의되어 있으면 해당 값을 사용한다.
      • 그렇지 않으면 /error를 사용한다.
      • 실습용 프로젝트에는 error.path가 정의되어 있지 않으므로 결과는 /error
    • ${server.error.path:A}
      • server.error.path 프로퍼티가 정의되어 있으면 해당 값을 사용한다.
      • 그렇지 않으면 ${error.path:/error}의 결과를 사용한다.
      • 실습용 프로젝트에는 server.error.path가 정의되어 있지 않으므로 결과는 /error

${server.error.path:${error.path:/error}}는 삼항 연산자와 비슷하다. BasicErrorController는 위와 같은 과정을 거쳐 /error라는 요청을 매핑한다. 스프링 웹 MVC에서는 예외가 발생하면 /error를 요청하게 되면서 accept 헤더에 따른 메소드를 실행하고 응답하기 때문에 BasicErrorController가 예외를 처리할 수 있다.

BasicErrorController 커스터마이징

BasicErrorController는 ErrorController를 상속한다. BasicErrorController를 커스터마이징하고자 한다면 ErrorController를 구현하면 된다. ErrorController를 상속하는 클래스를 만들어서 빈으로 등록하면 해당 클래스가 BasicErrorController의 역할을 대신하게 된다.

BasicErrorController를 상속해서 BasicErrorController의 기능을 활용하면서 커스터마이징 클래스를 구현해도 된다.

커스텀 에러 페이지

커스텀 에러 페이지는 예외가 발생했을 때, 응답의 상태 코드값에 따라 다른 페이지를 보여줄 때 사용한다. 커스텀 에러 페이지는 src/main/resources/static|template/error/ 디렉토리에 있는 상태코드값.html(Ex. 400.html)이 된다. 특정한 상태코드가 아니라 범위를 넓게 잡고 싶으면 5xx.html과 같이 500번대의 에러 페이지를 지정할 수도 있다.

좀 더 많은 커스터 마이징이 필요하다면 ErrorViewResolver을 구현하면 된다.

해당 포스팅은 스프링 부트 개념과 활용 강의 내용을 토대로 작성하였습니다.