Spring MVC 예외처리

2020. 9. 3. 18:42 Spring Framework/Spring boot

Exception Handling in Spring MVC

Spring MVC는 예외처리를 위한 몇가지 훌륭한 접근법을 제공해주지만 Spring MVC를 가르칠때 학생들이 종종 헷갈려하거나 불편해한다는 것을 알았다.

이 글에서 이를 위해 사용가능한 다양한 옵션을 보여줄 것이다. 우리의 목표는 가능한한 컨트롤러 메소드에서 명시적으로 예외처리를 하지 않는 것이다. 이들의 횡단관심사cross-cutting concern는 전용코드에서 별도로 처리하는 더 나은 방식을 제공해준다.

3가지 옵션이 있다: 예외별, 컨트롤러별, 전역별 per exception, per controller or globally.

이 글에서 다루어진 예제 어플리케이션은 다음의 주소에서  받아볼 수 있다

http://github.com/paulc4/mvc-exceptions.

아래의 Sample Application 섹션에 자세한 설명을 해두었다..

알림: 데모 어플리케이션은 2014년 10월에 새롭게 다시 업데이트되어 스프링 부트 1.1.8에서 사용가능하며, 사용하거나 이해하기 더 쉽게 바뀌었다.

HTTP 상태코드 사용하기 Using HTTP Status Codes

보통 웹요청 처리시 발생하는 미처리 예외unhandled exception는 서버가 HTTP 500 응답을 리턴한다. 그러나 당신이 작성한 커스텀 예외를 (HTTP명세서에 의해 정의된 모든 HTTP 상태코드를 지원하는) @ResponseStatus 어노테이션과 함께 사용할 수 있다.  어노테이션된 예외annotated exception가 컨트롤러 메소드에서 발생할 때, 그리고 다른 곳에서 처리되지 않을때, 이는 자동적으로 특정한 상태코드를 가지고 리턴되는 적절한 HTTP응답을 발생할 것이다.

예를 들면, 여기 Order가 빠진 경우의 예외exception를 보자:

    @ResponseStatus(value=HttpStatus.NOT_FOUND, reason="No such Order")  // 404
    public class OrderNotFoundException extends RuntimeException {
        // ...
    }

그리고 그것을 사용하고 있는 컨트롤러:

    @RequestMapping(value="/orders/{id}", method=GET)
    public String showOrder(@PathVariable("id") long id, Model model) {
        Order order = orderRepository.findOrderById(id);
        if (order == null) throw new OrderNotFoundException(id);
        model.addAttribute(order);
        return "orderDetail";
    }

만일 이 메소드에 유효하지않은 Order ID가 들어오면 익숙한 HTTP 404 응답이 리턴되어질 것이다.

컨트롤러 기반 예외처리 Controller Based Exception Handling

@ExceptionHandler 사용하기 Using @ExceptionHandler

당신은 부가적인 (@ExceptionHandler) 메소드를 어느 컨트롤러에나 추가하여 같은 컨트롤러의 요청처리request handling (@RequestMapping) 메소드에 의해 발생하는 예외처리들을 구체화할 수 있다. 이러한 메소드들은 다음의 일들을 할 수 있다:

  1. @ResponseStatus 어노테이션 없이 예외를 처리한다 (보통 당신이 작성하지않은 선정의된 예외들)
  2. 사용자를 특정한 에러페이지로 리다이렉트한다
  3. 완전히 컨스텀 에러 응답을 만든다

아래의 컨트롤러는 이 3가지 옵션을 보여준다:

@Controller
public class ExceptionHandlingController {

  // @RequestHandler methods
  ...
  
  // Exception handling methods
  
  // Convert a predefined exception to an HTTP Status code
  @ResponseStatus(value=HttpStatus.CONFLICT, reason="Data integrity violation")  // 409
  @ExceptionHandler(DataIntegrityViolationException.class)
  public void conflict() {
    // Nothing to do
  }
  
  // Specify the name of a specific view that will be used to display the error:
  @ExceptionHandler({SQLException.class,DataAccessException.class})
  public String databaseError() {
    // Nothing to do.  Returns the logical view name of an error page, passed to
    // the view-resolver(s) in usual way.
    // Note that the exception is _not_ available to this view (it is not added to
    // the model) but see "Extending ExceptionHandlerExceptionResolver" below.
    return "databaseError";
  }

  // Total control - setup a model and return the view name yourself. Or consider
  // subclassing ExceptionHandlerExceptionResolver (see below).
  @ExceptionHandler(Exception.class)
  public ModelAndView handleError(HttpServletRequest req, Exception exception) {
    logger.error("Request: " + req.getRequestURL() + " raised " + exception);

    ModelAndView mav = new ModelAndView();
    mav.addObject("exception", exception);
    mav.addObject("url", req.getRequestURL());
    mav.setViewName("error");
    return mav;
  }
}

이 메소드들중 아무거나, 당신이 추가적인 처리를 위해 고를 수 있다. 가장 일반적인 예제는 예외를 로그하는 것이다.

처리Handler 메소드는 유연한 특징을 가지고 있어 당신이 HttpServletRequestHttpServletResponseHttpSession 그리고/또는 Principl. 와 같은 명확히 서블릿 관련 객체들을 패스할 수 있다. 중요한 알림:  Model 은 @ExceptionHandler 메소드의 파라메터가 될 수 없다. 대신, 위의 handleError()에 의해 보여진.ModelAndView 를 사용하여 메소드안에 하나의 모델을 설정할 수 있다.

예외와 뷰 Exceptions and Views

예외를 모델에 추가할 때 조심해야할 점은, 당신의 사용자들은 구체적인 자바 예외나 stack-trace가 포함된 웹페이지를 보고 싶지않아 한다는 것이다. 하지만 페이지 소스안에 코멘트로서 구체적인 예외 상태를 넣어 당신을 서포트하는 사람들을 도와주려는 것은 유용할 수 있다. 만일 JSP를 사용한다면 당신은 아래와 같이 예외 메세지나 (숨겨진 <div> 를 사용하여) stack-trace를 출력하는 등등을 할 수 있을 것이다.

    <h1>Error Page</h1>
    <p>Application has encountered an error. Please contact support on ...</p>
    
    <!--
    Failed URL: ${url}
    Exception:  ${exception.message}
        <c:forEach items="${exception.stackTrace}" var="ste">    ${ste} 
    </c:forEach>
    -->

타임리프Thymeleaf에서 이와 같은 일을 하려면 support.html를 보자
예제 어플리케이션에선 다음과 같은 결과를 볼 수 있다.


전역 예외 처리 Global Exception Handling

@ControllerAdvice 클래스 사용하기 Using @ControllerAdvice Classes

컨트롤러 어드바이스는 당신에게 똑같은 예외처리 기술을 사용하지만, 개별 컨트롤러가 아니라 전체 어플리케이션에 적용할 수 있게 만들어 준다. 이들을 어노테이션 기반 인터셉터annotation driven interceptor로 이해하면 될 것이다.

@ControllerAdvice 어노테이션을 가지는 클래스는 컨트롤러 어드바이스controller-advice가 되며 3가지 타입을 메소드를 지원할 수 있다:

  • @ExceptionHandler으로 어노테이션된 예외처리 메소드
  • @ModelAttribute으로 어노테이션된 (추가적인 데이터를 모델에 추가하기 위한) 모델 향상Model enhancement 메소드. [Note] 이들 속성들은  예외처리 뷰에서 사용할 수 없다.
  • @InitBinder로 어노테이션된 (폼처리를 설정하는데 사용되는) 바인더 초기화Binder initialization 메소드 

우리는 여기서 예외처리만 다룰것이므로 @ControllerAdvice 메소드에 대한 자세한 사항은 온라인 메뉴얼을 보자.

위에서 본 어느 예외처리도 컨트롤러-어드바이스 클래스에서 정의할 수 있다 - 그러나 이제 이들은 이제 모든 컨트롤러에서 발생하는 예외에 적용될 것이다. 아래 간단한 예제를 보자:

@ControllerAdvice
class GlobalControllerExceptionHandler {
    @ResponseStatus(HttpStatus.CONFLICT)  // 409
    @ExceptionHandler(DataIntegrityViolationException.class)
    public void handleConflict() {
        // Nothing to do
    }
}

어떠한 예외에 처리되는 기본 처리자가 필요하면, 다음과 같이 약간만 손보면 된다. 어노테이션된 예제는 프레임워크에 의해 처리된다는 것을 명심하자:

@ControllerAdvice
class GlobalDefaultExceptionHandler {
    public static final String DEFAULT_ERROR_VIEW = "error";

    @ExceptionHandler(value = Exception.class)
    public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {
        // If the exception is annotated with @ResponseStatus rethrow it and let
        // the framework handle it - like the OrderNotFoundException example
        // at the start of this post.
        // AnnotationUtils is a Spring Framework utility class.
        if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null)
            throw e;

        // Otherwise setup and send the user to a default error-view.
        ModelAndView mav = new ModelAndView();
        mav.addObject("exception", e);
        mav.addObject("url", req.getRequestURL());
        mav.setViewName(DEFAULT_ERROR_VIEW);
        return mav;
    }
}

더 자세히 들어가보기 Going Deeper

HandlerExceptionResolver

HandlerExceptionResolver를 구현한 DispatcherServlet의 application context에서 선언된 모든 스프링빈은 MVC 시스템에서 올라오는 모든 예외를 처리하고 인터셉트하는데 사용되어지며 컨트롤러에 의해 처리되지않는다. 인터페이스틑 아래와 같다:

public interface HandlerExceptionResolver {
    ModelAndView resolveException(HttpServletRequest request, 
            HttpServletResponse response, Object handler, Exception ex);
}

handler 는 예외가 발생한 컨트롤러를 참조한다. (@Controller 인스턴스들은 스프링 MVC가 지원하는 핸들러의 하나의 타입일 뿐이라는 것을 기억하자. 예를 들면, HttpInvokerExporter 와 WebFlow Executor 또한 핸들러의 타입들이다.

이 뒷단에서 MVC는 세가지 resolver를 기본으로 생성한다. 이 3가지 리졸버들은 위에 논의돈 행동들을 구현한 것이다:

  • ExceptionHandlerExceptionResolver는 핸들러(컨트롤러)와 컨트롤러-어드바이스들상의 적절한 @ExceptionHandler 메소드를 위한 uncaught exception에 맞닿아 매치된다.
  • matches uncaught exceptions against for
    suitable @ExceptionHandler methods on both the handler (controller) and on any controller-advices.
  • ResponseStatusExceptionResolver는 (섹션1에서 설명한) @ResponseStatus 에 의해 어노테이션 된 uncaught exception들을 찾는다.
  • DefaultHandlerExceptionResolver 는 표준 스프링 예외를 변환하고 그들을 HTTP상태코드로 변환한다. (스프링MVC에서 내부적으 동작하는 부분에 대해서 언급하지는 않겠다)

이들은 서로 순서에 따라 연쇄작동하고 처리한다. (내부적으로 스프링은 이를 담당하는 빈들 생성하는데 - HandlerExceptionResolverComposite이 이를 담당한다)

resolveException 의 메소드 시그니쳐는 Model을 포함하지않는다는 것을 상기하자. 아래에 그 이유가 있다


필요시 자신의 커스텀 예외처리 시스템을 설정하기 위해, 커스텀

`HandlerExceptionResolver`를 구현할 수 있다. Handlers 는 보통 스프링의 `Ordered`인터페이스를 구현하여 당신이 실행하는 핸들러의 순서를 정의 할수 있다.


###SimpleMappingExceptionResolver

Spring has long provided a simple but convenient implementation of `HandlerExceptionResolver`
that you may well find being used in your appication already - the `SimpleMappingExceptionResolver`.
It provides options to:

  * Map exception class names to view names - just specify the classname, no package needed.
  * Specify a default (fallback) error page for any exception not handled anywhere else
  * Log a message (this is not enabled by default).
  * Set the name of the `exception` attribute to add to the Model so it can be used inside a View
(such as a JSP). By default this attribute is named ```exception```.  Set to ```null``` to disable.  Remember
that views returned from `@ExceptionHandler` methods _do not_ have access to the exception but views
defined to `SimpleMappingExceptionResolver` _do_.

Here is a typical configuration using XML:
<bean id="simpleMappingExceptionResolver"
      class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
    <property name="exceptionMappings">
        <map>
            <entry key="DatabaseException" value="databaseError"/>
            <entry key="InvalidCreditCardException" value="creditCardError"/>
        </map>
    </property>
    <!-- See note below on how this interacts with Spring Boot -->
    <property name="defaultErrorView" value="error"/>
    <property name="exceptionAttribute" value="ex"/>

    <!-- Name of logger to use to log exceptions. Unset by default, so logging disabled -->
    <property name="warnLogCategory" value="example.MvcLogger"/>
</bean>

Or using Java Configuration:

@Configuration
@EnableWebMvc // Optionally setup Spring MVC defaults if you aren’t doing so elsewhere
public class MvcConfiguration extends WebMvcConfigurerAdapter {
@Bean(name=“simpleMappingExceptionResolver”)
public SimpleMappingExceptionResolver createSimpleMappingExceptionResolver() {
SimpleMappingExceptionResolver r =
new SimpleMappingExceptionResolver();

    Properties mappings = new Properties();
    mappings.setProperty("DatabaseException", "databaseError");
    mappings.setProperty("InvalidCreditCardException", "creditCardError");

    r.setExceptionMappings(mappings);  // None by default
    r.setDefaultErrorView("error");    // No default
    r.setExceptionAttribute("ex");     // Default is "exception"
    r.setWarnLogCategory("example.MvcLogger");     // No default
    return r;
}
...

}


The _defaultErrorView_ property is especially useful as it ensures any uncaught exception generates a suitable application defined error page. (The default for most application servers is to display a Java stack-trace - something your users should _never_ see). ###Extending SimpleMappingExceptionResolver It is quite common to extend `SimpleMappingExceptionResolver` for several reasons: * Use the constructor to set properties directly - for example to enable exception logging and set the logger to use * Override the default log message by overriding `buildLogMessage`. The default implementation always returns this fixed text:<ul style="margin-left: 2em"><i>Handler execution resulted in exception</i></ul> * To make additional information available to the error view by overriding `doResolveException` For example:

public class MyMappingExceptionResolver extends SimpleMappingExceptionResolver {
public MyMappingExceptionResolver() {
// Enable logging by providing the name of the logger to use
setWarnLogCategory(MyMappingExceptionResolver.class.getName());
}

@Override
public String buildLogMessage(Exception e, HttpServletRequest req) {
    return "MVC exception: " + e.getLocalizedMessage();
}

@Override
protected ModelAndView doResolveException(HttpServletRequest request,
        HttpServletResponse response, Object handler, Exception exception) {
    // Call super method to get the ModelAndView
    ModelAndView mav = super.doResolveException(request, response, handler, exception);

    // Make the full URL available to the view - note ModelAndView uses addObject()
    // but Model uses addAttribute(). They work the same. 
    mav.addObject("url", request.getRequestURL());
    return mav;
}

}


This code is in the demo application as <a href="https://github.com/paulc4/mvc-exceptions/blob/master/src/main/java/demo1/web/ExampleSimpleMappingExceptionResolver.java">ExampleSimpleMappingExceptionResolver</a> ###Extending ExceptionHandlerExceptionResolver It is also possible to extend `ExceptionHandlerExceptionResolver` and override its `doResolveHandlerMethodException` method in the same way. It has almost the same signature (it just takes the new `HandlerMethod` instead of a `Handler`). To make sure it gets used, also set the inherited order property (for example in the constructor of your new class) to a value less than `MAX_INT` so it runs _before_ the default ExceptionHandlerExceptionResolver instance (it is easier to create your own handler instance than try to modify/replace the one created by Spring). See <a href="http://github.com/paulc4/mvc-exceptions/blob/master/src/main/java/demo1/web/ExampleExceptionHandlerExceptionResolver.java">ExampleExceptionHandlerExceptionResolver</a> in the demo app for more. ###Errors and REST RESTful GET requests may also generate exceptions and we have already seen how we can return standard HTTP Error response codes. However, what if you want to return information about the error? This is very easy to do. Firstly define an error class:

public class ErrorInfo {
public final String url;
public final String ex;

public ErrorInfo(String url, Exception ex) {
    this.url = url;
    this.ex = ex.getLocalizedMessage();
}

}


Now we can return an instance from a handler as the ```@ResponseBody``` like this:

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MyBadDataException.class)
@ResponseBody ErrorInfo handleBadRequest(HttpServletRequest req, Exception ex) {
return new ErrorInfo(req.getRequestURL(), ex);
}
```

언제 무엇을 써야하나? What to Use When?

보통 스프링은 당신에게 선택할 수 있게 제공하는 것을 선호한다. 그래서 당신이 해야하는게 뭘까? 여기 몇가지 중요한 룰이 있다. 하지만 XML설정이나 어노테이션을 선호한다면 그 역시 상관없다.

  • 당신이 작성한 예외들에 @ResponseStatus를 추가하는 것을 고려하라.
  • @ControllerAdvice 클래스에 @ExceptionHandler 메소드를 구현하거나 SimpleMappingExceptionResolver의 인스턴스를 사용하는 모든 종류의 예외들에 대해 아마 당신의 어플리케이션 설정에 이미  SimpleMappingExceptionResolver 를 이미 사용하고 있다면, 여기에 새로운 예외클래스를 추가하는게 @ControllerAdvice를 구현하는 것보다 더 쉬울 것이다.
  • 컨트롤러 특정 예외 처리를 하려면 당신의 컨트롤러에 @ExceptionHandler 메소드를 추가하자.
  • Warning: 같은 어플리케이션에 이들 옵션을 너무 많이 혼용하여 사용하지 않아야한다. 같은 예외같 하나 이상의 방식으로 처리되어질 수 있고, 이경우 원치않는 행동을 얻을 수 있다. 컨트롤러에서 @ExceptionHandler 메소드는 항상 @ControllerAdvice 인스턴스의 메소드들 전에 선택되어진다. 무슨 컨트롤러 어드바이스가 먼저 처리되는지 정의하지 않는다.

예제 어플리케이션 Sample Application

예제 어플리케이션은 github에서 받을 수 있다.
스프링 부트와 타임리프를 사용하는 간단한 웹어플리케이션이다.

이 어플리케이션은 2014년 10월에 더 이해하기 쉽게 개정되었다. 그 기반은 동일하다. 스프링부트 1.1.8과 스프링 4.1을 사용하지만 스프링 3.x에서 또한 동작가능하다.

이 데모는 클라우드 파운드리의 http://mvc-exceptions-v2.cfapps.io/에서 동작하고 있다.

데모에 대해 About the Demo

어플리케이션은 서로 다른 예외처리 기술을 쓰는 5개의 데모페이지를 가지고 있다:

  1. 그 자신의 예외처리를 위한 @ExceptionHandler 메소드를 가진 하나의 컨트롤러 
  2. 글로벌 컨트롤러 어드바이저에 의해 처리되는 예외를 뿌리는 하나의 컨트롤러
  3. SimpleMappingExceptionResolver 를 사용하여 예외처리
  4. 3번과 동일하지만 비교를 위해 SimpleMappingExceptionResolver를 disabled 함
  5. 어떻게 스프링 부트가 에러페이지를 만드는지 보여줌

홈 웹페이지는 index.html 이며:

  • 각 데모페이지로의 링크
  • 스프링부트에 관심있는 사람을 위해 스프링 부트 종단의 링크

각각 데모페이지는 몇개의 링크를 가지고 있으며 모두 예외를 발생시킨다. 당신 브라우저 백버튼을 사용하여 각 데모페이지로 되돌아 올수 있다.

이 데모를 내장 톰켓 컨테이너에서 자바 어플리케이션으로 실행할 수 있게 만든 스프링 부트에 감사한다. 이 어플리케이션을 싱핼하려면 다음중 하나의 명령어를 사용하면 된다:

  • mvn exec:java
  • mvn spring-boot:run

기본 홈페이지 URL은 http://localhost:8080.

스프링 부트와 에러처리 Spring Boot and Error Handling

스프링 부트 는 스프링 프로젝트를 최소한의 설정으로 돌릴 수 있게 만들어준다. 스프링부트는 클래스 패스의 키 클래스과 패키지들을 찾아 자동으로 민감한 기본값들을 생성한다. 예를 들면 당신이 서블릿 환경을 사용중이라면 스프링 MVC를 가장 일반적으로 많이 사용하는 뷰-리졸버view-resolvers, 핸들러 매핑handler mappings 등등을 설정해준다. 만일 JSP나 타임리프 파일이 있으면 그에 맞는 해당 뷰 기술을 자동으로 설정한다.

스프링 MVC는 기본적으로 제공하는 에러페이지가 없다. 기본 에러페이지를 설정하는 가장 흔한 방법은 언제나 SimpleMappingExceptionResolver 를 가지는 것이다. (스프링 버전1 이후로) 그러나 스프링 부트는 또한 에러 처리fallback error-handling 페이지를 제공하고 있다.

시작시 스프링 부트는 /error를 위한 매핑을 찾는다. 명명법에 의해 /error 로 끝나는 URL은 같은 이름의 논리적 뷰와 매핑된다: error. 데모 어플리케이션에서, 이 뷰는 타임리프 템플릿의  error.html로 매핑된다. (만일 JSP를 사용하고 있다면 
InternalResourceViewResolver가 설정되면서 error.jsp로 매핑될 것이다)

/error 와 매핑되는 뷰가 없다면, 스프링 부트는 “Whitelabel Error Page” 라 불리는 그 자신의 에러페이지를 정의한다 (HTTP상태 정보와 uncaught exception로 부터의 메세지와 같은 어떠한 에러 디테일 가지는 최소한의 페이지). 만일 error.html 템플릿을 이를테면, error2.html로 이름을 바꾸고 재시작하면 이것이 사용되어지는 것을 확인할 수 있다.

defaultErrorView()로 불리는 @Bean 메소드를 자바 설정으로 정의함으로서, 당신은 자신만의 에러 View인스턴스를 리턴할 수 있다. (더 자세한 정보는 스프링 부트의ErrorMvcAutoConfiguration 클래스를 보자)

기본 에러 뷰를 설정하기 위해 이미  SimpleMappingExceptionResolver를 사용중이라면? 간단히 defaultErrorView에 스프링부트에서 사용하는 같은 뷰: error를 정의해주면 된다. 또는 application.properties파일에 error.whitelabel.enabled를 false로 설정하여 스프링 부트의 기본 에러페이지를 disabled하면 된다. 이경우 당신의 컨테이너의 기본 에러페이지가 사용될 것이다.

Main의 생성자에서 스프링 부트 프로퍼티를 설정하는 예제링크

이 데모에서 SimpleMappingExceptionResolver의 defaultErrorView 프로퍼티는 의도적으로 error이 아니라 defaultErrorPage로 설정되어 당신은 핸들러가 에러페이지를 생성할때나 스프링 부트가 응답할때 볼 수 있을 것이다. 보통은 둘다 error로 설정되어 있다.

또한 이 데모 어플리케이션에서 stack-trce를 HTML소스에 숨겨둔 서포트 준비가 된support-ready 에러페이지를 만드는 법을 확인할 수 있다. (코멘트로서). 이상적으로 이러한 정보는 로그로 부터 얻어야 하지만 실제 삶은 언제나 이상적이지 않는다. 어쨋든 이 페이지에서 보려주려고 하는 것은 어떻게 존재하는 에러처리 메소드인 handleError 가 그 자신에 추가적인 정보를 제공하기 위해  ModelAndView를 만드는 가 이다. 다음의 사항도 확인해보자:

ExceptionHandlingController.handleError() on github
GlobalControllerExceptionHandler.handleError() on github



출처: https://springboot.tistory.com/25?category=620230 [스프링부트는 사랑입니다]