Server/Spring

Spring - REST에서 예외를 처리하는 다양한 방법!

JaeHoney 2022. 7. 3. 11:11

Spring에서 예외 처리를 구현하는 다양한 방법에 대해 알아본다.

 

Spring 3.2 이전에는 Spring MVC 애플리케이션에서 예외를 처리하는 대표적인 두 가지 방식이 있었다. HandlerExceptionResolver 또는 @ExceptionHandler 애노테이션이었다. 해당 방법들은 분명한 단점이 있었다.

 

Spring 3.2 부터는 이전의 두 솔루션의 단점을 극복하고 통합 예외 처리를 유도하기 위해 @ControllerAdvice 애노테이션을 사용했다.

 

그리고 Spring 5부터 REST API에서 기본 예외를 처리하기 위해 ResponseStatusException을 제공한다. 이들은 관심사 분리를 훌륭하게 처리한다.

Solution 1. @ExceptionHandler

Controller Level에서 동작하는 방식이다. 예외를 처리하는 메서드를 정의하고 @ExceptionHandler 애노테이션을 추가한다.

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

해당 방식에는 큰 단점이 있다. @ExceptionHnadler 주석이 달린 메서드는 전체 응용 프로그램에 전역이 아니라 해당 Controller에 대해서만 활성화된다. 이를 모든 컨트롤러에 추가하면 불필요한 코드가 중복되게 된다.

 

이를 해결하려면 기본 컨트롤러 클래스를 만들어서 이를 상속받으면 된다. 다만 클래스 설계가 복잡해진다는 단점이 있다.

Solution 2. HandlerExceptionResolver

ExceptionHandlerExceptionResolver

ExceptionHandlerExceptionResolver를 사용하면 애플리케이션에서 발생하는 모든 예외를 핸들링할 수 있다. 해당 Resolver는 Spring 3.1에 도입되었으며 DispatcherServlet에서 기본적으로 활성화하고 있다.

 

이는 앞에서 설명한 @ExceptionHandler와 동일한 매커니즘으로 동작한다.

DefaultHandlerExceptionResolver

Spring 3.0에 도입된 방식이며 DisplatcerServlet에서 기본적으로 활성화하고 있다. 클라이언트 오류 4XX 및 서버 오류 5XX 상태 코드에 대한 표준 Spring 예외를 해결하는 데 사용된다.

 

단점은 응답 본문에 아무것도 줄 수 없다는 것이다. 응답 본문이 있어야 실패 원인을 사용자에게 제공할 수 있다.

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보다 깔끔하고 응답에 상태 코드를 매핑하지만 본문은 여전히 null이다.

Custom HandlerExceptionResolver

DefaultHandlerExcpetionResolver와 ResponseStatusExceptionResolver는 REST API 서비스를 제공하는 데 큰 도움이 된다. 하지만 응답 본문을 제어할 수 없다는 큰 단점이 있다.

 

커스텀 예외 리졸버를 만들면 응답을 내려주는 것이 가능해진다.

@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 헤더 값을 고려할 수 있다. ModelAndView를 통해 응답 본문도 클라이언트에게 제공할 수 있다.

 

다만 저수준의 HttpServletResponse를 다뤄야한다는 점과 이전 MVC 모델에서 사용하는 ModelAndView를 사용한다는 점은 현재로써 적합하지 않다.

Solution 3. @ControllerAdvice

Spring 3.2부터 제공되는 @ControllerAdvice 애노테이션은 전역으로 @ExceptionHandler를 적용하는 기능을 제공한다.

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

이는 @ExceptionHandler의 안전성과 유연성과 함께 ResponseEntity를 활성화해주는 매커니즘을 제공한다.

 

다음은 @ControllerAdvice를 사용할 때 얻을 수 있는 이점이다.

  • 응답 본문과 상태 코드를 완전히 컨트롤할 수 있다.
  • 여러 예외를 동일한 메서드에 매핑하여 일관된 처리를 할 수 있도록 한다.
  • 새로운 RESTFul ResponseEntity를 잘 활용할 수 있게 유도한다.
  • @ExceptionHandler를 전역으로 올리면서 불필요한 코드 중복과 변경점이 생기는 것을 막을 수 있다.

Solution 4. ResponseStatusException

Spring 5.0 부터 ResponseStatusException을 도입했다. 해당 방식을 사용하면 Status Code와 Message가 포함된 인스턴스를 만들 수 있다.

@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와 전체 규칙으로 적용하는 것은 어렵다. 즉, Global 단위로는 @ControllerAdvice를 구현하고 특정 범위에서는 ResponseStatusException을 로컬로 구현하면 예외를 우아하게 핸들링할 수 있게 된다.

 


Reference