[SrpingBoot] RestAPI에 대한 에러 핸들링

2022. 11. 18. 15:53JAVA/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, 에러 페이지 연결