본문 바로가기

IT

[SpringBoot] 에러 처리 Errorcontroller(@ExceptionHandler)

728x90
{
  "timestamp": "2019-02-15T22:24:41.275+0000",
  "status": 404,
  "error": "Not Found",
  "message": "No message available",
  "path": "/123",
  "nice": "springboot"
}

에러 처리는 애플리케이션 개발에 있어 여러 오류를 빠르게 파악하고 수정하는데 있어 유용하게 사용되어 의무가 아닌 필수가 되었다. 

 

기본적으로 스프링부트 애플리케이션을 만들고 실행하면 404 Not Found에 대해서 다음과 같은 화면을 볼 수 있다.

404 Not Found 화면 에러 처리

같은 요청 localhost:8080/123에 대해서 포스트맨을 사용하여 JSON 응답값을 확인해보면 다음과 같다.

{
  "timestamp": "2023-08-14T22:21:24.237+0000",
  "status": 404,
  "error": "Not Found",
  "message": "No message available",
  "path": "/123"
}

spring boot에서 기본적으로 오류처리에 대한 properties는 다음과 같다.

# spring boot의 기본 properties
server.error:
  include-exception: false
  include-stacktrace: never # 오류 응답에 stacktrace 내용을 포함할 지 여부
  path: '/error' # 오류 응답을 처리할 Handler의 경로
  whitelabel.enabled: true # 서버 오류 발생시 브라우저에 보여줄 기본 페이지 생성 여부

Spring Boot는 AbstractErrorController를 상속받는 BasicErrorController에서 server.error.path 혹은 error.path로 등록된 property의 값을 넣거나, 없는 경우 /error를 사용한다.

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

  @Override
  public String getErrorPath() {
    return this.errorProperties.getPath();
  }

  @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
  public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
            
    HttpStatus status = getStatus(request);
    Map<String, Object> model = 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);
  }

  @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);
  }    
}

HTML로 응답을 주는 경우 errorHtml에서 응답을 처리한다. HTML 외의 응답이 필요한 경우 error에서 처리한다. getErrorAttributes를 통해 응답을 위한 모델을 생성하고 에러 처리에 대한 응답 값을 생성한다.

 

스프링은 놀랍게도 에서 ErrorAttribues를 인터페이스를 제공하고 기본적으로 DefaultErrorAttributes를 사용하고 있다.

우리는 스프링이 제공해주는 이런 확장된 포인트를 마음껏 누릴 수 있다. DefaultErrorAttributes를 상속받는 커스텀한 Attributes를 만들어서 사용할 수 있다.

@Component
public class CustomErrorAttributes extends DefaultErrorAttributes {

    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        Map<String, Object> result = super.getErrorAttributes(webRequest, includeStackTrace);
        result.put("nice", "springboot");
        return result;
    }
}
{
  "timestamp": "2023-08-14T22:21:24.237+0000",
  "status": 404,
  "error": "Not Found",
  "message": "No message available",
  "path": "/123",
  "nice": "springboot"
}

ErrorAttributes와 마찬가지로 ErrorController의 구현체를 개발자가 bean으로 등록한다면 Spring Boot는 해당 빈을 먼저 찾아 BasicErrorController 대신 오류 처리를 위해 사용하게 된다. 위임자 패턴을 사용해서 기본적인 처리는BasicErrorController에게 위임하고 나머지 필요한 처리를 추가할 수 있다. 스프링에서 제공해주는 대부분의 기능들이 이러한 위임자 패턴을 이용하여 스프링 내부 코어 개발에 열려있다. 

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

    public CustomErrorController(ErrorAttributes errorAttributes,
                                 ServerProperties serverProperties,
                                 List<ErrorViewResolver> errorViewResolvers) {
        super(errorAttributes, serverProperties.getError(), errorViewResolvers);
    }

    @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView errorHtml(HttpServletRequest request,
                                  HttpServletResponse response) {
        log(request); // 로그 추가
        return super.errorHtml(request, response);
    }

    @Override
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        log(request);
        return super.error(request);
    }

    private void log(HttpServletRequest request) {
        log.error("error");
    }

로그를 추가해서 확인하면 새로 만든 CustomErrorController가 잘 적용된 것을 확인할 수 있다.

 

ErrorController가 동작하는 것은 요청을 처리해야할 Servlet에서 오류가 발생했으나 해당 Servlet에서 오류를 처리하지 않아서 Servlet Container까지 오류가 전파되었을 때 ServletException으로 래핑되어 Servlet Container가 ErrorController를 호출한다. 

Spring에서는 Handler(Controller의 @RequestMapping이 걸린 메서드)에서 처리하다 Exception이 발생한 경우, 이를 Servlet Container까지 전파하지 않고, 직접 Exception 별로 처리를 할 수 있도록 @ExceptionHandler를 제공해준다. 

@RestController
@RequestMapping("/api")
public class BoardController {

  @GetMapping("/{id}")
  public Board get(@PathVariable Long id) {
    if (id < 1L) {
      throw new TotoroNotFoundException("invalid id: " + id);
    }
    return new Totoro("id", "content");
  }

  @ResponseStatus(HttpStatus.NOT_FOUND)
  @ExceptionHandler(TotoroNotFoundException.class)
  public Map<String, String> handle(TotoroNotFoundException e) {
      log.error(e.getMessage(), e);
      Map<String, String> errorAttributes = new HashMap<>();
      errorAttributes.put("code", "Totoro_NOT_FOUND");
      errorAttributes.put("message", e.getMessage());
      return errorAttribute;
  }
}

Controller에서 예외가 발생한 경우, Spring은 @ExceptionHandler를 검색하여 해당 애너테이션에 선언된 예외 및 하위 예외에 대해서 특정 메서드가 처리할 수 있도록 한다. Spring에서는 Bean으로 등록되는 @Controller들을 선택적으로, 혹은 전역으로 몇가지 공통 설정을 적용할 수 있도록 @ControllerAdvice를 사용할 수 있다. @ControllerAdvice에서 사용할 수 있는 것 중 하나가 @ExceptionHandler 이다.

@Slf4j
@Order(ORDER)
@RestControllerAdvice(annotations = RestController.class)
public class GlobalRestControllerAdvice {

    public static final int ORDER = 0;

    @ResponseStatus(HttpStatus.NOT_FOUND)
    @ExceptionHandler(TotoroNotFoundException.class)
    public Map<String, String> handle(TotoroNotFoundException e) {
        log.error(e.getMessage(), e);
        Map<String, String> errorAttributes = new HashMap<>();
        errorAttributes.put("code", "Totoro_NOT_FOUND");
        errorAttributes.put("message", e.getMessage());
        return errorAttributes;
    }
}

@Slf4j
@Order(GlobalRestControllerAdvice.ORDER + 1)
@ControllerAdvice
public class GlobalHtmlControllerAdvice {

    @ResponseStatus(HttpStatus.NOT_FOUND)
    @ExceptionHandler(TotoroNotFoundException.class)
    public String handle(TotoroNotFoundException e, Model model, HttpServletRequest request) {
        log.error(e.getMessage(), e);
        model.addAttribute("timestamp", LocalDateTime.now());
        model.addAttribute("error", "Totoro_NOT_FOUND");
        model.addAttribute("path", request.getRequestURI());
        model.addAttribute("message", e.getMessage());
        return "/error/404";
    }
}

애너테이션 기반으로 동작하는 @ExceptionHandler 외에도 HandlerExceptionResolver 인터페이스를 사용할 수 있다.

 

정리하자면 Spring MVC 내에서는 @ExceptionHandler를 통해 각 @Controller 별로 예외 처리를 할 수 있으며 @ExceptionHandler를 @ControllerAdvice에 등록하여 전역적으로 예외를 처리할 수 있다. 이러한 기본 동작들은 HandlerExceptionResolver에 의해 이루어지며 Spring MVC 내에서 처리하지 못한 예외들은 ServletException으로 포장되어 서블릿 컨테이너까지 전파되며 서블릿 컨테이너는 예외를 처리하기 위한 경로로 예외 처리를 위함한다. 이 때 Default는 BasicErrorController가 이를 담당하며 이를 커스텀해보는 시간을 가져보았다.

 

추가적으로 Spring MVC에서 Filter -> DispatcherServlet -> Interceptor 구조를 잘 이해해보면 Interceptor는 DispatcherServlet 내부에서 발생하기에 ControllerAdvice를 적용할  수 있지만 Filter는 DispatcherServlet 외부에서 발생하기에 ErrorController에서 처리해야 한다. 

 

 

 

 

'IT' 카테고리의 다른 글

Java heap dump, JVM OOM(Out of Memory)  (1) 2024.01.09
Aurora Failover에 대한 WAS 전략  (1) 2023.10.27
[Java] MDC를 활용한 로그 추적  (0) 2023.08.04
[Java] ThreadLocal  (0) 2023.07.31
CQRS ? AbstractRoutingDatasource  (0) 2023.07.29