ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring REST 에서의 Global Exception
    기록해야 기억한다/Spring 2021. 1. 11. 12:26

    spring-mvc 에서의 global exception 에 대해 메모해놓으려합니다. 언제나 그랬듯이 spring ref. 문서를 찾는게 가장 좋으니 간략하게 정리하는 수준이 될 것입니다.

    Spring 3.2 이전에는 Spring MVC application 에서 예외를 처리하는 두 가지 주요 접근 방식이 있었습니다. 하나는 HandlerExceptionResolver, 또 하나는 @ExceptionHandler annotation 이였습니다. 둘다 분명한 단점이 있습니다.

    spring-boot 에서는 WebMvcRegistrations Bean 을 선언하고 이를 재정의하여 custom instance 로 제공하는 방법으로 사용할 수 있습니다.

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

    최신 배포버전인 Spring 5는 REST API에서 기본 오류 처리를 위한 빠른 방법인 ResponseStatusException 클래스를 도입하였습니다.

    DispatcherServlet 의 Special Bean 형태들

    Spring-MVC DispatcherServlet 은 몇 가지 special bean 들을 감지합니다.(https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-servlet-special-bean-types) 이를 통해, Exception 을 해결할 수 있는 전략을 구성할 수 있는데 HandlerExceptionResolver 라는 Bean 으로 정의됩니다.

    Exceptions 의 관리

    우선 Exception 의 관리를 알아보려합니다. Exception 의 처리를 맡고 있는 HandlerExceptionResolver 는 DispatcherServlet 에서 아래와 같은 경우에 발생합니다.

    • request mapping 중에 예외가 발생한 경우
    • request handler(@Controller 같은)에서 예외가 발생한 경우

    위의 경우에서 DispatcherServlet 은 예외를 해결하고 일반적으로 오류 응답인 대체처리를 제공하기 위해 HandlerExceptionResolver Bean 을 Chain에 추가하여 위임합니다.

    HandlerExceptionResolver 의 구현 클래스

    아래의 Resolver 들은 기본적으로 DispatcherSevlter 에 의해 활성화 되어 동작합니다.

    HandlerExceptionResolver Description
    SimpleMappingExceptionResolver Exception class name 과 Error View name 간의 매핑입니다. 브라우저 애플리케이션에서 error page를 렌더링하는 데 유용합니다. REST 서비스와는 관련이 없습니다. Spring 3.2 부터 ExceptionHandlerExceptionResolver 에 의해 deprecated 되었습니다.
    DefaultHandlerExceptionResolver Spring MVC 에서 발생한 예외를 해결하고 이를 HTTP 상태 코드에 매핑합니다. 대체 ResponseEntityExceptionHandler 및 REST API Exception(https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-rest-exceptions)도 참조하십시오.
    한가지 제한 사항은 Response Body 에는 아무것도 설정하지 않습니다. 그래서 REST API 서비스에 어울리지 않습니다.
    ResponseStatusExceptionResolver @ResponseStatus annotation 으로 예외를 해결하고 annotation 의 값을 기반으로 HTTP 상태 코드에 매핑합니다.
    마찬가지로 Response Body 는 여전히 null 로 처리됩니다.
    ExceptionHandlerExceptionResolver @Controller 또는 @ControllerAdvice 클래스에서 @ExceptionHandler 메서드를 호출하여 예외를 해결합니다. @ExceptionHandler 메서드를 참조하십시오.
    REST API 를 이용하는 Service 에 대한 일반적인 요구 사항은 Response Body 에 '오류 세부 정보' 를 포함하는 것입니다.
    Response Body 의 '오류 세부 정보' 표현이 애플리케이션에 따라 다르기 때문에 Spring Framework는 이를 자동으로 수행하지 않습니다. 그런데, @RestController는 ResponseEntity 반환 값과 함께 @ExceptionHandler 메서드를 사용하여 응답의 상태와 본문을 설정할 수 있습니다. 이러한 메서드는 @ControllerAdvice (@RestControllerAdvice) 클래스에서 선언하여 전역적으로 적용 할 수도 있습니다.

    Chain of Resolvers - resolver 체인

    DispathcerServlet 은 내부적으로 chain 구조로 구성되어 절차적(Order 가 높으면 나중에 실행)으로 servlet 업무를 수행합니다.

    HandlerExceptionResolver의 계약은 다음을 반환할 수 있음을 지정합니다.

    • error view 를 가리키는 ModelAndView
    • resolver 안에서 예외가 처리된 경우 empty ModelAndView
    • 예외가 해결되지 않은 상태로 유지되고 후속 resolver 가 try 할 수 있는 경우 null이며, 예외가 끝에 남아 있으면 Servlet 컨테이너로 버블 업 할 수 있습니다.

    MVC Config 은 기본 Spring MVC exception, @ResponseStatus annotation 이 달린 예외 및 @ExceptionHandler 메소드 지원에 대한 내장 resolver 를 자동으로 선언합니다. 이를 개발자는 custom 하거나 바꿀 수 있습니다.

    Error Page

    예외가 HandlerExceptionResolver 에 의해 해결되지 못하고 그대로 남아 전파될 수 있는 상태이거나, error status(4xx, 5xx) 로 설정된 경우 servlet 컨테이너는 HTML 에서 기본 오류 페이지를 렌더링 할 수 있습니다. 이럴 때 아래와 같이 error-page 를 정의하여 ERROR dispatch 를 수행합니다.

    <error-page> 
    	<location>/error</location> 
    </error-page>

    위에 지정된 URL 인 "/error" 를 처리하기 위한 Controller 를 지정하여 해당 오류를 렌더링할 수 있습니다.

    @RestController 
    public class ErrorController { 
        @RequestMapping(path = "/error") 
        public Map<String, Object> handle(HttpServletRequest request) { 
        	Map<String, Object> map = new HashMap<String, Object>(); 
            map.put("status", request.getAttribute("javax.servlet.error.status_code")); 
            map.put("reason", request.getAttribute("javax.servlet.error.message")); 
            return map; 
        } 
    }

     

    아래부터는 몇가지 방식의 예외처리를 위한 방법을 언급할 것입니다. 염두에 두어야 할 사항은 가능하면 서비스 기준으로 항상 한가지 방식으로 특정 종류의 예외 처리 방법을 가져가는 것이 좋습니다.

    첫 번째 처리방법

    첫 번째는 @Controller 및 @ControllerAdvice 클래스안에서 예외를 처리하는 메서드를 정의하고 @ExceptionHandler annotation 을 추가하는 방법입니다. 그러면 위에서 언급한 ExceptionHandlerExceptionResolver (Spring Dispatcher 에 의해서 기본적으로 활성화 되어있음)에 의해 빌드되어 처리됩니다.

    @Controller 
    public class SimpleController { 
    	// ... 
    	@ExceptionHandler 
    	public ResponseEntity<String> handle(IOException ex) { 
    		// ... 
    	} 
    }

    해당 방법은 top-level exception 과 매치되며, 위의 예에서 직접적으로 메서드 인수로 선언된 IOException 에 대한 예외를 처리합니다. 다른 Exception 안에서 발생한 IOException 이 대상이 되지는 않습니다. 여러 Exception 이 일치하는 경우는 일반적으로 cause exception 보다 root exception 을 우선합니다.(안쪽에서 ExceptionDepthComparator 이 사용됩니다.) 그래서 일반적으로 argument signature 를 가능한 한 구체적으로 지정하여 root 및 cause exception types 간의 불일치 가능성을 줄이는 것이 좋습니다.

    annotation 정의에서 다음 예제와 같이 일치하도록 예외 유형을 좁힐 수 있습니다.

    @ExceptionHandler({FileSystemException.class, RemoteException.class}) 
    public ResponseEntity<String> handle(IOException ex) { 
    	// ... 
    } 
    
    @ExceptionHandler({FileSystemException.class, RemoteException.class}) 
    public ResponseEntity<String> handle(Exception ex) { 
    	// ... 
    }

    @ExceptionHandler 메서드는 아래와 같은 arguments (method parameter) 를 지원합니다.

    • Exception type - 발생한 exception 에 access 하기 위함
    • HandlerMethod - 예외를 발생시킨 컨트롤러 메서드에 액세스하기 위함
    • WebRequest, NativeWebRequest - Servlet API 가 아닌 request parameters, request and session attributes 액세스
    • ServletRequest, ServletResponse - Servlet 기반의 Request, Response 액세스
    • java.servlet.http.HttpSession
    • java.security.Principal
    • HttpMethod
    • Locale, timeZone, ZoneId
    • java.ioOutputStream, Writer - Servlet API에 의해 노출된 raw response body 에 액세스하기 위함
    • RedirectAttributes
    • @SessionAttribute - session attribute 에 액세스
    • @RequestAttriBute - request attribute 에 액세스

    @ExceptionHandler 메서드의 반환형식은 일반적인 Controller 의 반환형식과 유사합니다.(https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-exceptionhandler-return-values)

    이 방법은 Controller 가 다른 jar 에 있거나 직접 수정할 수 없는 다른 기본 class 에서 이미 확장되었거나 직접 수정할 수 없을 경우는 사용할 수 없습니다.


    두 번째 처리방법

    HandlerExceptionResolver를 정의하여 사용하는 방법이 있습니다. 이러면 application 에서 발생한 모든 예외에 대한 resolver 를 구성할 수 있습니다.

    HandlerExceptionResolver 의 재구성

    보통은 client 측에서 Accept 헤더를 통해 요청한 정보대로 response 를 내려주는게 가장 이상적인 경우이고, 그렇지 않을 경우 서비스의 장애로 이어질 수 있습니다.

    @Component public class RestResponseStatusExceptionResolver 
    	extends AbstractHandlerExceptionResolver { 
        @Override 
        protected ModelAndView doResolveException(HttpServletRequest request, 
        	HttpServletResponse response, Object handler, Exception ex) { 
            try { 
            	// Exception 의 종류별로 return ModelAndView 생성하여 처리 
                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; 
        } 
        
        // ModelAndView 를 반환하기에 필요한 무엇이든 설정 가능함 
        private ModelAndView handleIllegalArgument(IllegalArgumentException ex, 
        	HttpServletResponse response) throws IOException { 
        	response.sendError(HttpServletResponse.SC_CONFLICT); 
            String accept = request.getHeader(HttpHeaders.ACCEPT); 
            ... 
            // PrintWriter 를 통해 accept 값에 따라 별도의 구성 가능 
            // writer = response.getWriter(); 
            // writer.write(사용자 정의 body); 
            // writer.flush(); 
            // writer.close(); 
            return new ModelAndView();
        } 
    }

    HttpServletResponse 와 상호작용하고 ModelAndView 를 사용하는 MVC 모델에 적합합니다.


    세 번째 처리방법

    Spring 3.2는 @ControllerAdvice annotation 으로 전역 @ExceptionHandler 지원합니다. 이를 통해 이전 MVC 모델에서 벗어나 @ExceptionHandler 의 형식 안전성 및 유연성과 함께 ResponseEntity 를 사용하는 메커니즘을 사용할 수 있습니다.(https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-controller-advice)

    일반적으로 @ExceptionHandler, @InitBinder, @ModelAttribute 메서드등은 @Controller 클래스안에서 적용됩니다. 이러한 메서드들을 컨트롤러간에 더 전역적으로 적용하려면 @ControllAdvice 또는 @RestControllerAdvice 로 annotation 이 달린 클래스에서 선언할 수 있습니다.

    Spring 이 시작할 때 @RequestMapping 및 @ExceptionHandler 메서드의 인프라 클래스는 @ControllerAdvice 로 주석이 달린 Spring Bean을 감지한 다음 런타임에 해당 메서드를 적용합니다. @ControllerAdvice 의 전역 @ExceptionHandler 메서드는 @Controller 의 로컬 메서드 이후에 적용됩니다. 반대로 전역 @ModelAttribute 및 @InitBinder 메서드는 로컬 메서드보다 먼저 적용됩니다.

    기본적으로 @ControllerAdvice 메서드는 모든 요청 (즉, 모든 컨트롤러)에 적용되지만 다음 예제와 같이 주석의 속성을 사용하여 컨트롤러의 하위 집합으로 범위를 좁힐 수 있습니다.

    // Target all Controllers annotated with 
    @RestController 
    @ControllerAdvice(annotations = RestController.class) 
    public class ExampleAdvice1 {
    } 
    
    // Target all Controllers within specific packages 
    @ControllerAdvice("org.example.controllers") 
    public class ExampleAdvice2 {} 
    
    // Target all Controllers assignable to specific classes 
    @ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class}) 
    public class ExampleAdvice3 {}

    네 번째 처리방법

    ResponseStatusException (Spring 5 이상) 을 throw 로 던지는 방법이 있습니다. 전체규칙을 적용하는 것은 어렵지만, proto type 을 생성하거나 다른 방법에 비해 선언하기 쉽고, 하나의 Exception 에 대해 메서드마다 다른 응답을 발생할 수 있는 이점이 있습니다.

    @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);
        }
    }
    반응형
    LIST

    댓글

Designed by Tistory.