그날그날메모

Spring을 사용한 REST 오류 처리

Java

Spring을 사용한 REST 오류 처리

그날그날메모 2021. 3. 7. 01:56

1. 개요

이 튜토리얼은 REST API를 위해 Spring을 사용하여 예외 처리를 구현하는 방법을 설명 합니다. 우리는 또한 약간의 과거 개요를 얻고 다른 버전이 도입 한 새로운 옵션을 볼 것입니다.

Spring 3.2 이전에 Spring MVC 애플리케이션에서 예외를 처리하는 두 가지 주요 접근 방식은 HandlerExceptionResolver 또는 @ExceptionHandler 어노테이션이었습니다. 둘 다 분명한 단점이 있습니다.

3.2부터 이전 두 솔루션의 한계를 해결하고 전체 애플리케이션에서 통합 된 예외 처리를 촉진하기 위해 @ControllerAdvice 어노테이션 이 있습니다.

이제 Spring 5는 REST API에서 기본적인 오류 처리를위한 빠른 방법 인 ResponseStatusException  클래스를 소개합니다  .

이들 모두는 한 가지 공통점이 있습니다. 그들은 우려분리를 매우 잘 처리합니다. 앱은 어떤 종류의 실패를 나타 내기 위해 일반적으로 예외를 throw 할 수 있으며, 그런 다음 별도로 처리됩니다.

마지막으로 Spring Boot가 테이블에 제공하는 내용과 필요에 맞게 구성하는 방법을 살펴 보겠습니다.

2. 솔루션 1 : 컨트롤러 수준 @ExceptionHandler

첫 번째 솔루션은 @Controller 수준 에서 작동 합니다. 예외를 처리하는 메소드를 정의하고 @ExceptionHandler로 어노테이션을 달 것입니다 .

public class FooController{
    
    //...
    @ExceptionHandler({ CustomException1.class, CustomException2.class })
    public void handleException() {
        //
    }
}

이 방법은 큰 단점이있다 : T 그는 @ExceptionHandler 어노테이션이 방법은 특정 컨트롤러에만 활성화되어 전체 응용 프로그램이 아닌 전 세계적으로. 물론 이것을 모든 컨트롤러에 추가하면 일반적인 예외 처리 메커니즘에 적합하지 않습니다.

모든 컨트롤러가 기본 컨트롤러 클래스를 확장하도록 하여이 제한을 해결할 수 있습니다 .

그러나이 솔루션은 어떤 이유로 든 불가능한 애플리케이션의 경우 문제가 될 수 있습니다. 예를 들어 컨트롤러는 다른 jar에 있거나 직접 수정할 수없는 다른 기본 클래스에서 이미 확장되거나 직접 수정할 수 없을 수 있습니다.

다음으로, 예외 처리 문제를 해결하는 다른 방법을 살펴 보겠습니다. 하나는 전역 적이며 컨트롤러와 같은 기존 아티팩트에 대한 변경 사항을 포함하지 않습니다.

3. 솔루션 2 : HandlerExceptionResolver

두 번째 해결책은 HandlerExceptionResolver 를 정의하는 것 입니다. 이렇게하면 응용 프로그램에서 발생한 모든 예외가 해결됩니다. 또한 REST API에서 균일 한 예외 처리 메커니즘 을 구현할 수 있습니다.

커스텀 리졸버로 이동하기 전에 기존 구현을 살펴 보겠습니다.

3.1. ExceptionHandlerExceptionResolver

이 해석기는 Spring 3.1에서 도입되었으며 기본적으로 DispatcherServlet 에서 활성화됩니다 . 이것은 실제로 앞에서 제시 한 @ ExceptionHandler 메커니즘이 작동 하는 방식의 핵심 구성 요소입니다 .

3.2. DefaultHandlerExceptionResolver

이 리졸버는 Spring 3.0에서 도입되었으며 DispatcherServlet 에서 기본적으로 활성화되어 있습니다.

표준 Spring 예외를 해당 HTTP 상태 코드 , 즉 클라이언트 오류 4xx 및 서버 오류 5xx 상태 코드 로 해결하는 데 사용됩니다 . 다음은 처리하는 Spring 예외 전체 List 과 상태 코드에 매핑하는 방법입니다.

응답의 상태 코드를 적절하게 설정하지만 한 가지 제한은 Response body에 아무것도 설정하지 않는다는 것입니다. REST API의 경우 (상태 코드는 실제로 클라이언트에게 제공하기에 충분한 정보가 아닙니다.) 응답에도 본문이 있어야 응용 프로그램이 실패에 대한 추가 정보를 제공 할 수 있습니다.

이것은 ModelAndView를 통해 뷰 해상도를 구성하고 오류 콘텐츠를 렌더링하여 해결할 수 있지만 솔루션은 분명히 최적이 아닙니다. 이것이 Spring 3.2가 이후 섹션에서 논의 할 더 나은 옵션을 도입 한 이유입니다.

3.3. ResponseStatusExceptionResolver

이 해석기는 Spring 3.0에서도 도입되었으며 DispatcherServlet 에서 기본적으로 활성화되어 있습니다.

주요 책임은 사용자 지정 예외에서 사용할 수 있는 @ResponseStatus 어노테이션 을 사용하고 이러한 예외를 HTTP 상태 코드에 매핑하는 것입니다.

이러한 사용자 지정 예외는 다음과 같습니다.

@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class MyResourceNotFoundException extends RuntimeException {
    public MyResourceNotFoundException() {
        super();
    }
    public MyResourceNotFoundException(String message, Throwable cause) {
        super(message, cause);
    }
    public MyResourceNotFoundException(String message) {
        super(message);
    }
    public MyResourceNotFoundException(Throwable cause) {
        super(cause);
    }
}

DefaultHandlerExceptionResolver 와 동일 하게이 리졸버는 Response body을 처리하는 방식에 제한이 있습니다. 응답에 상태 코드를 매핑하지만 본문은 여전히 null입니다.

3.4. SimpleMappingExceptionResolverAnnotationMethodHandlerExceptionResolver

아마도 SimpleMappingExceptionResolver는 꽤 많은 시간 동안 주변되었습니다. 이전 Spring MVC 모델에서 나 왔으며 REST 서비스 와는 관련없습니다. 기본적으로 예외 클래스 이름을 매핑하여 이름을 보는 데 사용합니다.

AnnotationMethodHandlerExceptionResolver는 스루 예외 처리하기 위해 스프링 3.0에 도입 된 @ExceptionHandler의 어노테이션을하지만 의해 사용되지 않습니다 ExceptionHandlerExceptionResolver Spring 3.2로.

3.5. 사용자 지정 HandlerExceptionResolver

DefaultHandlerExceptionResolverResponseStatusExceptionResolver 의 조합은 Spring RESTful 서비스에 좋은 오류 처리 메커니즘을 제공하는 데 큰 도움이됩니다. 단점은 앞에서 언급했듯이 Response body을 제어 할 수 없다는 것입니다.

이상적으로는 클라이언트가 요청한 형식 ( Accept 헤더 를 통해)에 따라 JSON 또는 XML을 출력 할 수 있기를 원합니다 .

이것만으로도 새로운 사용자 정의 예외 해결 프로그램을 만드는 것이 정당합니다 .

@Component
public class RestResponseStatusExceptionResolver extends AbstractHandlerExceptionResolver {

    @Override
    protected ModelAndView doResolveException(
      HttpServletRequest request, 
      HttpServletResponse response, 
      Object handler, 
      Exception ex) {
        try {
            if (ex instanceof IllegalArgumentException) {
                return handleIllegalArgument(
                  (IllegalArgumentException) ex, response, handler);
            }
            ...
        } catch (Exception handlerException) {
            logger.warn("Handling of [" + ex.getClass().getName() + "] 
              resulted in Exception", handlerException);
        }
        return null;
    }

    private ModelAndView 
      handleIllegalArgument(IllegalArgumentException ex, HttpServletResponse response) 
      throws IOException {
        response.sendError(HttpServletResponse.SC_CONFLICT);
        String accept = request.getHeader(HttpHeaders.ACCEPT);
        ...
        return new ModelAndView();
    }
}

여기서 주목해야 할 한 가지 세부 사항은 요청 자체에 액세스 할 수 있으므로 클라이언트가 보낸 Accept 헤더 의 값을 고려할 수 있다는 것 입니다.

예를 들어 클라이언트가 application / json을 요청 하면 오류 조건이 발생한 경우 application / json으로 인코딩 된 Response body을 반환해야 합니다.

다른 중요한 구현 세부 사항은 ModelAndView를 반환 한다는 것  입니다. 이것은 응답의 본문 이며 필요한 것은 무엇이든 설정할 수 있습니다.

이 접근 방식은 Spring REST 서비스의 오류 처리를위한 일관되고 쉽게 구성 가능한 메커니즘입니다.

그러나 제한이 있습니다. 저수준 HtttpServletResponse상호 작용하고 ModelAndView 를 사용하는 이전 MVC 모델에 적합 하므로 여전히 개선의 여지가 있습니다.

4. 솔루션 3 : @ControllerAdvice

Spring 3.2는 @ControllerAdvice 어노테이션 으로 전역 @ExceptionHandler 를  지원합니다 .

이를 통해 이전 MVC 모델 에서 벗어나 @ExceptionHandler 의 형식 안전성 및 유연성과 함께 ResponseEntity사용하는 메커니즘을 사용할 수 있습니다 .

@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 하나의 글로벌 오류 처리 구성 요소에 앞에서들.

실제 메커니즘은 매우 간단하지만 매우 유연합니다.

  • 응답의 본문과 상태 코드를 완전히 제어 할 수 있습니다.
  • 함께 처리 할 수 ​​있도록 동일한 메서드에 대한 여러 예외 매핑을 제공합니다.
  • 최신 RESTful ResposeEntity 응답 을 잘 활용합니다 .

여기서 명심해야 할 한 가지는 @ExceptionHandler선언 된 예외를 메서드의 인수로 사용되는 예외 와 일치 시키는 것입니다.

이것들이 일치하지 않으면 컴파일러는 불평하지 않을 것입니다. – 그럴 이유도 없습니다. 그리고 Spring도 불평하지 않을 것입니다.

그러나 예외가 실제로 런타임에 throw 되면 예외 해결 메커니즘이 다음과 함께 실패합니다 .

java.lang.IllegalStateException: No suitable resolver for argument [0] [type=...]
HandlerMethod details: ...

5. 솔루션 4 : ResponseStatusException (Spring 5 이상)

Spring 5는 ResponseStatusException 클래스를 도입했습니다 .

HttpStatus 와 선택적으로 이유원인을 제공하는 인스턴스를 만들 수 있습니다 .

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

ResponseStatusException 을 사용하면 어떤 이점이 있습니까?

  • 프로토 타이핑에 탁월함 : 기본 솔루션을 매우 빠르게 구현할 수 있습니다.
  • 하나의 유형, 여러 상태 코드 : 하나의 예외 유형으로 인해 여러 개의 다른 응답이 발생할 수 있습니다. 이것은 @ExceptionHandler에 비해 긴밀한 결합을 입니다.
  • 많은 사용자 지정 예외 클래스를 만들 필요가 없습니다.
  • 우리는이 예외 처리를보다 효율적으로 관리 예외는 프로그래밍 방식으로 생성 할 수 있기를.

그리고 트레이드 오프는 어떻습니까?

  • 통합 된 예외 처리 방법은 없습니다 . 전역 접근 방식을 제공하는 @ControllerAdvice 와 달리 일부 애플리케이션 전체 규칙을 적용하는 것이 더 어렵습니다 .
  • 코드 복제 : 우리는 여러 컨트롤러에서 코드를 복제 할 수 있습니다.

또한 하나의 애플리케이션 내에서 서로 다른 접근 방식을 결합 할 수도 있습니다.

예를 들어 @ControllerAdvice를 전역 적으로 구현할 수 있지만 ResponseStatusException을 로컬로 구현할  수도  있습니다.

그러나주의해야합니다. 동일한 예외를 여러 방법으로 처리 할 수있는 경우 몇 가지 놀라운 동작을 발견 할 수 있습니다. 가능한 규칙은 항상 한 가지 방식으로 특정 종류의 예외를 처리하는 것입니다.

자세한 내용과 추가 예제 ResponseStatusException에 대한 사용방법를 참조하십시오 .

6. Spring Security에서 거부 된 액세스 처리

액세스 거부는 인증 된 사용자가 액세스 할 권한이없는 리소스에 액세스하려고 할 때 발생합니다.

6.1. MVC — 사용자 정의 오류 페이지

먼저 솔루션의 MVC 스타일을 살펴보고 액세스 거부에 대한 오류 페이지를 사용자 지정하는 방법을 살펴 보겠습니다.

XML 구성 :

<http>
    <intercept-url pattern="/admin/*" access="hasAnyRole('ROLE_ADMIN')"/>   
    ... 
    <access-denied-handler error-page="/my-error-page" />
</http>

그리고 Java 구성 :

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/admin/*").hasAnyRole("ROLE_ADMIN")
        ...
        .and()
        .exceptionHandling().accessDeniedPage("/my-error-page");
}

사용자가 충분한 권한없이 리소스에 액세스하려고하면 "/ my-error-page" 로 리디렉션됩니다 .

6.2. 사용자 지정 AccessDeniedHandler

다음으로 사용자 정의 AccessDeniedHandler 를 작성하는 방법을 살펴 보겠습니다 .

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle
      (HttpServletRequest request, HttpServletResponse response, AccessDeniedException ex) 
      throws IOException, ServletException {
        response.sendRedirect("/my-error-page");
    }
}

이제 XML 구성을 사용하여 구성 해 보겠습니다 .

<http>
    <intercept-url pattern="/admin/*" access="hasAnyRole('ROLE_ADMIN')"/> 
    ...
    <access-denied-handler ref="customAccessDeniedHandler" />
</http>

0r Java 구성 사용 :

@Autowired
private CustomAccessDeniedHandler accessDeniedHandler;

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/admin/*").hasAnyRole("ROLE_ADMIN")
        ...
        .and()
        .exceptionHandling().accessDeniedHandler(accessDeniedHandler)
}

우리의 방법을 참고 CustomAccessDeniedHandler 우리가 리디렉션 또는 사용자 지정 오류 메시지를 표시하여 원하는대로, 우리는 응답을 사용자 정의 할 수 있습니다.

6.3. REST 및 메서드 수준 Security

마지막으로 메서드 수준 Security @PreAuthorize , @PostAuthorize@Secure Access Denied 를 처리하는 방법을 살펴 보겠습니다 .

물론, 앞서 논의한 전역 예외 처리 메커니즘을 사용하여 AccessDeniedException처리합니다 .

@ControllerAdvice
public class RestResponseEntityExceptionHandler 
  extends ResponseEntityExceptionHandler {

    @ExceptionHandler({ AccessDeniedException.class })
    public ResponseEntity<Object> handleAccessDeniedException(
      Exception ex, WebRequest request) {
        return new ResponseEntity<Object>(
          "Access denied message here", new HttpHeaders(), HttpStatus.FORBIDDEN);
    }
    
    ...
}

7. 스프링 부트 지원

Spring Boot는 합리적인 방식으로 오류를 처리하기 위해 ErrorController 구현을 제공합니다  .

간단히 말해서 브라우저 용 폴백 오류 페이지 (Whitelabel 오류 페이지라고도 함)와 RESTful 비 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"
}

평소와 같이 Spring Boot는 속성으로 이러한 기능을 구성 할 수 있습니다.

  • server.error.whitelabel.enabled : Whitelabel 오류 페이지를 비활성화하고 서블릿 컨테이너에 의존하여 HTML 오류 메시지를 제공하는 데 사용할 수 있습니다.
  • server.error.include-stacktrace : 항상  값으로; HTML 및 JSON 기본 응답 모두에 스택 추적을 포함합니다.
  • server.error.include-message :  버전 2.3부터 Spring Boot는 민감한 정보 유출을 방지하기 위해 응답에서 메시지 필드를 숨 깁니다  . 이 속성을 always 과 함께 사용  하여 활성화 할 수 있습니다.

이러한 속성 외에도 Whitelabel 페이지를 재정 의하여 / 오류에  대한 자체 뷰-리졸버 매핑을 제공 할 수 있습니다 .

컨텍스트에 ErrorAttributes 을 포함하여 응답에 표시하려는 속성을 사용자 정의 할 수도 있습니다  . 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  빈을 등록 할 수 있습니다  .

다시 말하지만, Spring Boot에서 제공 하는 기본 BasicErrorController를  사용  하여 도움을 줄 수 있습니다.

예를 들어, 애플리케이션이 XML 엔드 포인트에서 트리거 된 오류를 처리하는 방법을 사용자 지정하려고한다고 가정 해보십시오. 우리가해야 할 일은 @RequestMapping을 사용하여 퍼블릭 메소드를 정의하고  그것이 application / xml 미디어 유형을 생성한다고 명시하는 것입니다 .

@Component
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) {
        
    // ...

    }
}

참고 : 여기서 우리는 여전히 ServerProperties  빈에 바인딩 된 프로젝트에서 정의되었을 수 있는 server.error. * 부트 속성 에 의존하고 있습니다 .

8. 결론

이 기사에서는 Spring에서 REST API에 대한 예외 처리 메커니즘을 구현하는 여러 방법에 대해 설명했습니다. 이전 메커니즘에서 시작하여 Spring 3.2 지원을 계속해서 4.x 및 5.x로 계속합니다.

항상 그렇듯이이 기사에 제시된 코드는 GitHub에서 사용할 수 있습니다 .

Spring Security 관련 코드는 spring-security-rest 모듈을 확인할 수 있습니다 .

참고
  • https://docs.spring.io/spring-framework/docs/current/reference/html
  • https://www.baeldung.com/exception-handling-for-rest-with-spring