Spring Boot 예외 처리

Spring Boot 예외 처리

Category
Published
August 11, 2024
Last updated
Last updated September 7, 2024
💡
이 포스트는 현재 작성중입니다

Spring Boot에서 예측하지 못한 예외가 발생했을 때(WAS의 에러 전달)

스프링 부트에서는 기본 설정인 WebMvcAutoConfiguration의 ExceptionHandlerExceptionResolver가 예외 처리를 담당하게 된다.
public class WebMvcAutoConfiguration { @Override protected ExceptionHandlerExceptionResolver createExceptionHandlerExceptionResolver() { if (this.mvcRegistrations != null) { ExceptionHandlerExceptionResolver resolver = this.mvcRegistrations .getExceptionHandlerExceptionResolver(); if (resolver != null) { return resolver; } } return super.createExceptionHandlerExceptionResolver(); } }

BasicErrorController

스프링 부트는 예외 발생시 기본적으로 /error로 에러 요청을 다시 전달하도록 WAS 설정이 되어 있다. /error로 보내진 요청은 BasicErrorController 컨트롤러에 전달된다.
BasicErrorController는 요청의 accept 헤더에 따라 에러 페이지나 메시지를 반환한다.
@Controller @RequestMapping({"${server.error.path:${error.path:/error}}"}) public class BasicErrorController extends AbstractErrorController { private final ErrorProperties errorProperties; public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties) { this(errorAttributes, errorProperties, Collections.emptyList()); } public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties, List<ErrorViewResolver> errorViewResolvers) { super(errorAttributes, errorViewResolvers); Assert.notNull(errorProperties, "ErrorProperties must not be null"); this.errorProperties = errorProperties; } @RequestMapping( produces = {"text/html"} ) public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { HttpStatus status = this.getStatus(request); Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); ModelAndView modelAndView = this.resolveErrorView(request, response, status, model); return modelAndView != null ? modelAndView : new ModelAndView("error", model); } @RequestMapping public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) { HttpStatus status = this.getStatus(request); if (status == HttpStatus.NO_CONTENT) { return new ResponseEntity(status); } else { Map<String, Object> body = this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.ALL)); return new ResponseEntity(body, status); } } @ExceptionHandler({HttpMediaTypeNotAcceptableException.class}) public ResponseEntity<String> mediaTypeNotAcceptable(HttpServletRequest request) { HttpStatus status = this.getStatus(request); return ResponseEntity.status(status).build(); } protected ErrorAttributeOptions getErrorAttributeOptions(HttpServletRequest request, MediaType mediaType) { ErrorAttributeOptions options = ErrorAttributeOptions.defaults(); if (this.errorProperties.isIncludeException()) { options = options.including(new ErrorAttributeOptions.Include[]{Include.EXCEPTION}); } if (this.isIncludeStackTrace(request, mediaType)) { options = options.including(new ErrorAttributeOptions.Include[]{Include.STACK_TRACE}); } if (this.isIncludeMessage(request, mediaType)) { options = options.including(new ErrorAttributeOptions.Include[]{Include.MESSAGE}); } if (this.isIncludeBindingErrors(request, mediaType)) { options = options.including(new ErrorAttributeOptions.Include[]{Include.BINDING_ERRORS}); } return options; } }
이때 error()errorHtml()getErrorAttributeOptions()을 호출해 반환할 에러 속성을 얻는다.
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { HttpStatus status = this.getStatus(request); Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.TEXT_HTML))); ... } @RequestMapping public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) { HttpStatus status = this.getStatus(request); if (status == HttpStatus.NO_CONTENT) { return new ResponseEntity(status); } else { Map<String, Object> body = this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.ALL)); return new ResponseEntity(body, status); } }

getErrorAttributeOptions()

protected ErrorAttributeOptions getErrorAttributeOptions(HttpServletRequest request, MediaType mediaType) { ErrorAttributeOptions options = ErrorAttributeOptions.defaults(); if (this.errorProperties.isIncludeException()) { options = options.including(new ErrorAttributeOptions.Include[]{Include.EXCEPTION}); } if (this.isIncludeStackTrace(request, mediaType)) { options = options.including(new ErrorAttributeOptions.Include[]{Include.STACK_TRACE}); } if (this.isIncludeMessage(request, mediaType)) { options = options.including(new ErrorAttributeOptions.Include[]{Include.MESSAGE}); } if (this.isIncludeBindingErrors(request, mediaType)) { options = options.including(new ErrorAttributeOptions.Include[]{Include.BINDING_ERRORS}); } return options; }
this.getErrorAttributeOptions() 호출시 ErrorAttributeOptions 타입의 오류 정보를 받게 된다. 여기엔 오류 발생 시간(timestamp), 에러 상태 코드(status), 에러 내용(error), 에러 발생 경로(path) 등이 포함된다.
추가적으로 더 자세한 에러 정보를 받기 위해서는 다음과 같은 방식으로 설정 할 수 있다.
server.error.include-message: always server.error.include-binding-errors: always server.error.include-stacktrace: always server.error.include-exception: false
이러한 설정으로도 유용한 에러 응답을 전달하기 어렵다면 추가적인 에러 처리 구성을 통해 커스터마이징 해야한다.

Spring 예외 처리 방법

Spring에서는 에러 처리라는 공통 관심사를 메인 로직에서 분리하기 위해, 예외 처리를 HandlerExceptionResolver 인터페이스를 통하여 추상화 했다. (전략 패턴을 사용)
💡
핸들러(handler)는 컨트롤러 클래스에서 요청을 처리하는 메소드나 컨트롤러 자체를 의미한다
HandlerExceptionResolver는 발생한 Exception을 catch하여 HTTP상태와 응답 메시지 등을 설정한다. 이렇게 처리가 됐을 때 WAS 입장에서는 해당 요청이 정상적인 응답인 것으로 인식되어, 위와 같은 WAS의 에러 전달이 진행되지 않는다.
public interface HandlerExceptionResolver { ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex); }
여기서 handler는 오류가 발생한 컨트롤러 객체이다. 예외가 던져졌을 때 디스패처 서블릿(dispatcher servlet) 까지 예외가 전달된다. 이 예외를 처리하기 위해 기본적으로 4가지 HandlerExceptionResolver 구현체들이 빈에 등록되어 있다.
  • DefaultErrorAttributes: 에러 속성을 저장하며 직접 예외를 처리하지는 않는다
  • DefaultHandlerExceptionResolver:  스프링 내부의 기본 예외들을 처리
  • ResponseStatusExceptionResolver: Http 상태 코드를 지정하는 @ResponseStatus 또는 ResponseStatusException가 적용된 예외를 처리
  • ExceptionHandlerExceptionResolver: 에러 응답을 위한 Controller나 ControllerAdvice에 있는 @ExceptionHandler를 처리
DefaultErrorAttributes는 직접 예외를 처리하지 않기에 성격이 다르고, 3가지 ExceptionResolver들이 컴포지드 패턴을 이용해 HandlerExceptionResolverComposite로 모아서 관리된다.
 
Spring에서는 ExceptionResolver를 동작 시키기위해 다음과 같은 도구들을 사용한다.
  • ResponseStatus
  • ResponseStatusException
  • ExceptionHandler
  • ConrollerAdvice, RestControllerAdvice

@ResponseStatus

package org.springframework.web.bind.annotation; import org.springframework.core.annotation.AliasFor; import org.springframework.http.HttpStatus; @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface ResponseStatus { @AliasFor("code") HttpStatus value() default HttpStatus.INTERNAL_SERVER_ERROR; @AliasFor("value") HttpStatus code() default HttpStatus.INTERNAL_SERVER_ERROR; String reason() default ""; }
ResponseStatus.class
@ResponseStatus는 HTTP 응답 status code를 설정하는 것을 도와준다.
즉 오류 발생시 에러 status code를 지정해 주는데 사용할 수 있다. 에러 status code를 지정하기 위해 @ResponseStatus는 다음과 같은 곳에 적용될 수 있다.
  • Exception 클래스
  • @ExceptionHandler
  • @ControllerAdvice
// Exception 클래스에 적용하는 예시 @ResponseStatus(code = HttpStatus.BAD_REQUEST) class CustomException extends RuntimeException {} // @ExceptionHandler 예시 // @ControllerAdvice 예시
위와 같은 Exception이 발생해서 catch되지 않은 체 컨트롤러 밖으로 넘어가면ResponseStatusExceptionResolver에 의해서 처리되는데, 이때 @ResponseStatus로 지정해준 status code가 반환된다.
다만, ResponseStatusExceptionResolver는 WAS까지 예외가 전달되어 처리된다.

ResponseStatusException

ResponseStatusException은 Spring 5부터 도입되어 @ResponseStatus의 대체제로 도입되었습니다.
@ResponseStatus로는 라이브러리처럼 직접 수정할 수 없는 예외에는 적용할 수 없었고, 어노테이션을 사용하기에 조건에 따라 동적으로 변경하기가 어려웠기 때문입니다.
package org.springframework.web.server; import org.springframework.context.MessageSource; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatusCode; import org.springframework.http.ProblemDetail; import org.springframework.lang.Nullable; import org.springframework.web.ErrorResponseException; public class ResponseStatusException extends ErrorResponseException { @Nullable private final String reason; public ResponseStatusException(HttpStatusCode status) { this(status, (String)null); } public ResponseStatusException(HttpStatusCode status, @Nullable String reason) { this(status, reason, (Throwable)null); } public ResponseStatusException(int rawStatusCode, @Nullable String reason, @Nullable Throwable cause) { this(HttpStatusCode.valueOf(rawStatusCode), reason, cause); } public ResponseStatusException(HttpStatusCode status, @Nullable String reason, @Nullable Throwable cause) { this(status, reason, cause, (String)null, (Object[])null); } protected ResponseStatusException(HttpStatusCode status, @Nullable String reason, @Nullable Throwable cause, @Nullable String messageDetailCode, @Nullable Object[] messageDetailArguments) { super(status, ProblemDetail.forStatus(status), cause, messageDetailCode, messageDetailArguments); this.reason = reason; this.setDetail(reason); } @Nullable public String getReason() { return this.reason; } public HttpHeaders getHeaders() { return this.getResponseHeaders(); } public ProblemDetail updateAndGetBody(@Nullable MessageSource messageSource, Locale locale) { super.updateAndGetBody(messageSource, locale); if (messageSource != null && this.getReason() != null && this.getReason().equals(this.getBody().getDetail())) { Object[] arguments = this.getDetailMessageArguments(messageSource, locale); String resolved = messageSource.getMessage(this.getReason(), arguments, (String)null, locale); if (resolved != null) { this.getBody().setDetail(resolved); } } return this.getBody(); } public String getMessage() { HttpStatusCode var10000 = this.getStatusCode(); return "" + var10000 + (this.reason != null ? " \"" + this.reason + "\"" : ""); } }
ResponseStatusException.class
@GetMapping("/product/{id}") public ResponseEntity<Product> getProduct(@PathVariable String id) { try { return ResponseEntity.ok(productService.getProduct(id)); } catch (NoSuchElementFoundException e) { throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Item Not Found"); } }

@ExceptionHandler

 

@ConrollerAdvice, @RestControllerAdvice

 

Reference