2022. 11. 18. 15:53ㆍJAVA/Spring
1. 개요
해당 글은 Spring REST API에 대한 예외 핸들링을 구현하는 방법에 대해서 소개합니다.
스프링 3.2 이전에 Spring MVC 애플리케이션의 에러 핸들링하는 두가지 방법은 다음과 같습니다.
- HandlerExceptionResolver
- @ExceptionHandler
스프링 3.2 이후에는 @ControllerAdvice 애노테이션을 사용하여 위 두가지 방법의 단점을 해결하고 전체 애플리케이션의 예외 처리를 하고 있습니다.
스프링 5에서는 ResponseStatusException 클래스를 소개합니다. 이것은 REST API에서 기본적인 에러를 다루는 가장 빠른 방법의 클래스입니다.
2. Solution1, 컨트롤러 레벨에서 @ExceptionHandler 사용하기
첫번째 솔루션은 @Controller레벨에서 작동합니다. 다음과 같이 @ExceptionHandler를 이용하여 예외(CustomException)들을 다루는 메서드를 정의합니다.
@Controller
public class FooController {
@ExceptionHandler({CustomException1.class, CustomException2.class})
public void handleException(){
//...
}
}
단점
- @ExceptionHandler가 적용된 handlerException 메서드는 전체 애플리케이션이 아닌 FooController에서만 작동됩니다.
3. Solution2, HandlerExceptionResolver 인터페이스
HandlerExceptionResolver 인터페이스
- 전체 애플리케이션에 발생하는 예외를 해결합니다.
- REST API에서 예외 처리를 구현할 수 있음
HandlerExceptionResolver 구현체
- ExceptionHandlerExceptionResolver
- DefaultHandlerExceptionResolver
- ResponseStatusExceptionResolver
3.1 ExceptionHandlerExceptionResolver
- Spring 3.1
- DispatcherServlet에서 기본적으로 활성화되어 있습니다.
- @ExceptionHandler 매커니즘이 작동하는 방식의 핵심 구성 요소
3.2 DefaultHandlerExceptionResolver
- Spring 3.0
- DispatcherServlet에서 기본적으로 활성화되어 있습니다.
- 클라이언트 에러인 4xx과 서버 에러인 5xx 상태 코드와 같은 HTTP Status Code들에 따른 표준 스프링 예외들을 해결하는데 사용됨
- 제한사항
- Response의 상태코드는 올바르게 설정되지만 Response 본문에 아무것도 설정하지 않음
REST API(상태 코드만으로는 클라이언트에게 보여주는 충분하지 못한 정보를 줌)의 경우 애플리케이션이 에러에 대한 추가적인 정보를 제공할 수 있도록 Response에도 본문이 있어야합니다.
위와 같은 제한사항은 ModelAndView를 통한 에러 내용을 렌더링하는 것과 view resolution을 설정하는 것으로써 해결할 수 있습니다. 그러나 이 방법은 최적화하지는 않습니다.
3.3 ResponseStatusExceptionResolver
- Spring 3.0
- DispatcherServlet에서 기본적으로 활성화되어 있습니다.
- 주 역할은 이용가능한 커스텀 예외들에 @ResponseStatus 애노테이션을 사용하는 것입니다. 그리고 이 커스텀 예외들을 HTTP 상태 코드들에 매핑시키는 것입니다.
커스텀 예외는 다음과 같이 정의할 수 있습니다.
@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class MyResourceNotFoundException extends RuntimeException{
public MyResourceNotFoundException() {
}
public MyResourceNotFoundException(String message) {
super(message);
}
public MyResourceNotFoundException(String message, Throwable cause) {
super(message, cause);
}
public MyResourceNotFoundException(Throwable cause) {
super(cause);
}
}
단점
- DefaultHandlerExceptionResolver와 동일하게 Response에 상태 코드를 매핑하지만 본문은 여전히 null입니다.
3.4 Custom HnadlerExceptionResolver
Custom HandlerExceptionResolver
- DefaultHandlerExceptionResolver와 ResponseStatusExceptionResolver의 조합
- Spring RESTful 서비스에 좋은 에러 처리 메커니즘을 제공하는데 큰 도움을 줌
- 단점은 응답 본문을 제어할 수 없음
- 하지만 이상적으로는 클라이언트가 Accept Header를 통해 요청하는 형식(format)에 따라 에러 발생 원인 같은 것을 JSON 또는 XML로 출력할 수 있어야 합니다.
다음과 같이 새로운 Custom Exception Resolver를 정의할 수 있습니다.
@Component
public class RestResponseStatusExceptionResolver extends AbstractHandlerExceptionResolver {
@Override
protected ModelAndView doResolveException(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("error");
if(handler != null){
HandlerMethod handlerMethod = (HandlerMethod) handler;
modelAndView.addObject("errorMethod", handlerMethod.getMethod().getName());
}
modelAndView.addObject("errorCause", ex.getCause());
modelAndView.addObject("errorClass", ex.getClass().getSimpleName());
modelAndView.addObject("errorMessage", ex.getMessage());
return modelAndView;
}
}
제한사항
- 로우 레벨의 HttpServletResponse와 상호작용하고 있음
- ModelAndView를 사용하는 이전 MVC 모델에 적합하므로 개선의 여지가 있음
4. Solution 3, @ControllerAdvice
@ControllerAdvice
- Spring 3.2
- 이전 MVC 모델에서 벗어나 @ExceptionHandler의 타입 세이프(Type Safety) 유연성과 함께 ResponseEntity를 사용하는 메커니즘이 가능함
@ControllerAdvice를 적용한 예외 핸들러는 다음과 같이 구현할 수 있습니다.
@ControllerAdvice
public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(value = {IllegalArgumentException.class, IllegalStateException.class})
protected ResponseEntity<Object> handleConflict(RuntimeException ex, WebRequest request){
String bodyOfResponse = "This should be application specific";
return handleExceptionInternal(ex, bodyOfResponse, new HttpHeaders(), HttpStatus.CONFLICT, request);
}
}
@ControllerAdvice를 사용하면 이전에 흩어져 있던 여러개의 @ExceptionHandler를 단일 전역 에러 처리 구성요소로 통합할 수 있습니다.
@ControllerAdvice 장점
- HTTP 상태 코드뿐만 아니라 Response 본문에 대한 제어 권한을 가질 수 있습니다.
- 여러 예외를 동일한 메서드에 매핑하여 함께 처리할 수 있습니다.
- 최신 RESTful Response Entity 응답을 잘 활용합니다.
5. Solution 4, ResponseStatusException (Spring 5 이상)
HttpStatus와 선택적으로 이유 및 원인을 제공하는 인스턴스를 생성할 수 있습니다.
다음과 같이 @ResponseStatus 애노테이션을 적용한 예외 클래스를 사용할 수 있습니다.
@GetMapping(value = "/{id}")
public Foo findById(@PathVariable("id") Long id, HttpServletResponse response) {
try {
Foo resourceById = RestPreconditions.checkFound(service.findOne(id));
eventPublisher.publishEvent(new SingleResourceRetrievedEvent(this, response));
return resourceById;
}
catch (MyResourceNotFoundException exc) {
throw new ResponseStatusException(
HttpStatus.NOT_FOUND, "Foo Not Found", exc);
}
}
- 리소스를 찾지 못하면 MyResourceNotFoundException 예외가 발생하여 쓰로잉합니다.
ResponseStatus 예외의 장점
- 기본적인 솔루션을 빠르게 구현할 수 있음
- 한개의 타입, 여러 상태 코드 : 한개의 예외 타입으로 여러개의 다른 반응이 나올 수 있습니다. 이렇게 하면 @ExceptionHandler에 비해 의존성이 줄어듭니다.
- 사용자 지정 예외 클래스를 많이 만들 필요가 없습니다.
- 예외들이 프로그래밍컬하게 생성될 수 있기 때문에 예외 핸들링을 통해서 더 많은 제어를 할 수 있습니다.
상충 관계(TradeOff)
- 예외를 처리하는 통합된 방법이 없음
- 전역적으로 제공하는 @ControllderAdvice와는 달리 일부 애플리케이션 전체의 규칙을 적용하기가 어려움
- 코드 중복 : 여러 컨트롤러에서 코드를 복제할 수 있음
주의사항
- 하나의 애플리케이션 내에서 여러가지 다른 예외 처리 방법들이 사용될 수 있음
- 예를 들어 @ControllerAdvice를 전역적으로 사용하고 ResponseStatusException들을 지역적으로 사용될 수 있음
6. Spring Security에서 접근 부인 처리(Handle the Access Denied in Spring Security)
접근 부인(Access Denied)은 인증된 사용자가 접근하기위해 충분한 인증들이 가지지 못한채 자원들에 접근할때 발생합니다.
6.1. REST and Method-Level Security
메서드 레벨 보안 애노테이션들인 @PreAuthorize, @PostAuthroize, @Secure에 의해서 쓰로잉된 접근 부인 예외를 처리하기 위한 방법을 소개합니다.
@ControllerAdvice
public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(value = {AccessDeniedException.class})
public ResponseEntity<Object> handleAccessDeniedException(Exception ex, WebRequest request){
return new ResponseEntity<>("Access denied message here", new HttpHeaders(), HttpStatus.FORBIDDEN);
}
}
7. Spring Boot Support
SpringBoot는 에러를 합법적인 방법으로 처리할 수 있는 ErrorController 구현을 제공합니다.
간단히 말해서, 브라우저용 폴백(fallback) 에러 페이지(Whitelabel Error Page)와 RESTful, non-HTML 요청에 대한 JSON 응답을 제공합니다.
{
"timestamp": "2019-01-17T16:12:45.977+0000",
"status": 500,
"error": "Internal Server Error",
"message": "Error processing the request!",
"path": "/my-endpoint-with-exceptions"
}
일반적으로 SpringBoot는 속성(properties)들과 함께 이 기능들을 설정하는 것을 허용합니다.
- server.error.whitelabel.enabled : Whitelabel Error Page를 비활성화하기 위해 사용되고 서블릿 컨테이너를 사용하여 HTML 오류 메시지를 제공할 수 있습니다.
- server.error.include-stacktrace : HTML과 JSON 기본 응답에 stacktrace를 포함합니다.
- server.error.include-message : 2.3 버전 이후로 SpringBoot는 민감한 정보를 피하기 위해 응답에 메시지 필드를 숨길 수 있습니다.
컨텍스트에서 ErrorAttribute들을 포함함으로써 응답에 보여주는 속성들을 설정할 수 있습니다. Spring Boot에 의해 제공되는 DefaultErrorAttributes 클래스를 상속하여 설정할 수 있습니다.
@Component
public class MyCustomErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, options);
errorAttributes.put("locale", webRequest.getLocale().toString());
errorAttributes.remove("error");
return errorAttributes;
}
}
애플레킹션이 특정 컨텐츠 타입에 대해서 에러들을 어떻게 처리하는지 정의하기를 원한다면 ErrorController 빈을 등록할 수 있습니다. @RequestMapping을 사용하여 public 메서드를 정의해야 합니다. 그리고 produces 값에 application/xml 미디어 타입을 설정합니다.
public class MyErrorController extends BasicErrorController {
public MyErrorController(ErrorAttributes errorAttributes, ServerProperties serverProperties) {
super(errorAttributes, serverProperties.getError());
}
@RequestMapping(produces = MediaType.APPLICATION_XML_VALUE)
public ResponseEntity<Map<String, Object>> xmlError(HttpServletRequest request){
//...
return null;
}
}
References
Error Handling for REST with Spring
Spring MVC ExceptionResolver, 에러 페이지 연결
'JAVA > Spring' 카테고리의 다른 글
[SpringBoot] Spring Security #2 OAuth2 (0) | 2022.11.20 |
---|---|
[SpringBoot] Spring Security #1 MVC, WebFlux Security (0) | 2022.11.20 |
[SpringBoot][WebMVC] 웰컴페이지와 파비콘 (0) | 2022.11.10 |
[SpringBoot][WebMVC] 웹 JAR (0) | 2022.11.10 |
[SpringBoot][WebMVC] 정적 리소스 지원 (0) | 2022.11.09 |