기록해야 기억한다/Spring

Spring REST 에서의 Global Exception

bj-lee 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