파란하늘의 지식창고
Published 2019. 4. 30. 13:31
Spring Boot 전역 에러 처리 Study/Java
반응형

Spring Framework 5.1.6.RELEASE, Spring Boot 2.1.4 .RELEASE 기준으로 작성됨


Spring Framework의 전역 에러 처리

Spring framework는 전역 에러를 처리하기 위해 아래의 인터페이스를 제공한다.

  제공되는 interface
servlet (webmvc) HandlerExceptionResolver
reacitve (webflux) WebExceptionHandler

Servlet 전역 에러 처리

HandlerExceptionResolver

Spring Web MVC - Dispatcher Servlet - Exceptions

handlerExceptionResolver는 servlet에서 전역 에러 처리를 하기 위해 제공되는 인터페이스이다.

public interface HandlerExceptionResolver {

    @Nullable
    ModelAndView resolveException(
            HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);

}

아래와 같은 구현체가 제공된다.

구현체 설명
SimpleMappingExceptionResolver 발생한 exception class 이름과 error view 이름을 맵핑해서 사용. 
boot 에서 기본 설정대상이 아님
DefaultHandlerExceptionResolver 기본적으로 사용되는 exception 들을 공통 처리해주기 위해 제공되는 resolver
ResponseStatusExceptionResolver ResponseStatusException이 throw 되거나 @ResponseStatus를 설정한 지점에서 발생한 에러를 처리
ExceptionHandlerExceptionResolver @Controller 또는 @ControllerAdvice에 선언된 @ExceptionHandler 을 통해 에러를 처리

SimpleMappingExceptionResolver를 제외한 3개의 ExceptionResolver가 Spring boot를 사용하면 기본 등록된다.

DefaultHandlerExceptionResolver는 스프링이 알아서 처리해주는 에러 처리 모음이기 때문에 따로 구현할 선택항목이 아니다.

SimpleMappingExceptionResolver를 구현해서 각 Exception에 대한 에러 페이지 처리를 사용할 수도 있다. 해당 방법은 가장 오래된 방법이라 많은 블로그 글이 있으며 여기서는 생략한다.

ResponseStatusExceptionResolver를 사용하는 방법은 해당 에러에 종속되거나 어노테이션을 각 메서드나 클래스에 지정해야 하기 때문에 전역으로 사용하기엔 좋은 방법은 아니다.

결국 현재 가장 편한 방법은 ExceptionHandlerExceptionResolver에 의해 에러 처리가 되도록 @ExceptionHandler를 사용하는 방법이다.

@ExceptionHandler 에러 처리 만들기

Spring Web MVC - Annotated Controllers - Exceptions

기본 사용 방법

아래와 같이 클래스를 작성하였다고 가정하자.

@Controller
public class SimpleController {

    // ...

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

위와 같이 작성되면 해당 Controller의 메소드에서 발생한 IOException은 위에 @ExceptionHandler로 선언된  handle 메서드를 통해 에러가 처리된다.

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

위와 같이 @ExceptionHandler에 대상 Exception 목록을 정의하여 처리 대상인 Exception 유형을 좀 더 좁힐 수 있다.

IOException 중 FileSystemException과 RemoteException 에 대해서만 해당 handle 메서드가 동작한다.

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

위와 같이 대상 Exception을 @ExceptionHandler로 정의하고 일반적인 에러 유형인 Exception이나 또는 상위인 Throwable을 매개변수로 사용할 수도 있다.

@ControllerAdvice와 같이 사용하기

@Controller에 정의된 @ExceptionHandler는 해당 controller에서만 동작한다.

모든 Controller에 대해 발생한 전역 에러에 대해 처리하는 @ExceptionHandler는 @ControllerAdvice와 같이 사용하면 된다.

@Controller
@ControllerAdvice
public class GlobalExceptionController {

    // ...

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

위와 같이 @ControllerAdvice로 선언한 controller내에 정의된 @ExceptionHandler는 모든 controller를 대상으로 동작한다.

controller 대상을 좁혀서 지정하고 싶은 경우 아래처럼 사용하면 된다.

// @RestController를 사용한 모든 controller 대상
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}

// 해당 패키지 내 모든 controller 대상
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}

// 해당 클래스 하위로 구현된 controller 대상
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}

@ExceptionHandler에서 지원하는 method arguments

@ExceptionHandler를 사용하면서 아래의 매개 변수들을 사용할 수 있다.

매개 변수 설명
Exception type 대상 Exception
HandlerMethod 예외를 발생시킨 controller method
WebRequest, NativeWebRequest Servlet API를 직접 사용하지않고 request parameter와 request, session attribute에 접근
javax.servlet.ServletRequest
javax.servlet.ServletResponse
ServletRequest, HttpServletRequest 또는 스프링의 MultipartRequest, MultipartHttpServletRequest와 같은 유형
javax.servlet.http.HttpSession  
javax.security.Principal  
HttpMethod  
java.util.Locale 현재 요청에 대한 Locale 정보
java.util.TimeZone
java.time.ZoneId
현재 요청에 대한 timezone 정보
java.io.OutputStream
java.io.Writer
 
java.util.Map
org.springframework.ui.Model
org.springframework.ui.ModelMap
error response를 위한 model. empty 상태임
RedirectAttributes  
@SessionAttribute  
@RequestAttribute  

@ExceptionHandler에서 지원하는 return values

@ExceptionHandler를 사용하면서 아래의 반환 값들을 사용할 수 있다.

반환 값  
@ResponseBody HttpMessageConverter 객체에 의해 변환되어 반환
HttpEntity<B>
ResponseEntity<B>
(header와 body를 포함한) 전체 응답 처리를 HttpMessageConverter 객체를 통해 변환하여 반환
String viewResolver를 통해 처리되는 view 이름
View  
java.util.Map
org.springframework.ui.Model
RequestToViewNameTranslator를 통해 암시적으로 결정된 view 이름을 사용하는 경우 반환 값으로 사용 가능
@ModelAttribute  
ModelAndView object ModelAndView를 반환, response status도 추가 가능
void  
Any other return value 위 열거한 값과 일치하지 않고 BeanUtils#isSimpleProperty에 의해 결정된 simple type이 아닌 경우 model에 추가할 model attribute로 처리됨. simple type인 경우 해결되지 않음

Reactive 전역 에러 처리

WebExceptionHandler

Spring WebFlux - Reactive Core - Exceptions

WebExceptionHandler는 reactive에서 에러 처리를 하기 위해 제공되는 인터페이스이다.

public interface WebExceptionHandler {

    Mono<Void> handle(ServerWebExchange exchange, Throwable ex);

}

아래와 같은 구현체가 제공된다.

구현체 설명
ResponseStatusExceptionHandler ResponseStatusException 유형에 대한 예외 처리
WebFluxResponseStatusExceptionHandler @ResponseStatus 가 선언된 경우에 대한 예외처리. ResponseStatusExceptionHandler의 확장

현재는 2가지만 제공하고 있고 따로 구현하여 사용하는 핸들러는 없는 것 같다.

@ExceptionHandler 에러 처리 만들기

Spring WebFlux - Annotated Controllers - Managing Exceptions

Servlet과 마찬가지로 @ControllerAdvice와 @ExceptionHandler를 사용하여 전역 처리가 가능하다.

@Controller
public class SimpleController {

    // ...

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

또한 매개 변수와 반환 값도 동일하게 사용 가능하다.

다만 request body나 @ModelAttribute에 관련된 매개변수와 반환 값은 사용 불가능하다. 

따라서 Servlet에서 사용 가능한 ModelAndView 같은 반환 값을 쓸 수 없다.

Spring Boot의 에러 처리

Servlet Error Handling

Reactive Error Handling

Spring Boot는 모든 오류를 적절한 방식으로 처리하여 /error로 매핑하는 전역 오류 페이지 등록을 제공한다.

또한 http 상태와 예외에 대한 적절한 메시지를 json으로 응답하거나 해당 내용을 html 형식으로 렌더링 하는 whitelabel 페이지 뷰를 제공한다.

이를 위해 아래의 인터페이스를 제공한다.

  제공되는 interface
servlet (webmvc) org.springframework.boot.web.servlet.error.ErrorAttributes
reacitve (webflux) org.springframework.boot.web.reactive.error.ErrorAttributes

두 인터페이스는 거의 유사하다.

// servlet ErrorAttributes
public interface ErrorAttributes {
    Map<String, Object> getErrorAttributes(WebRequest webRequest,
            boolean includeStackTrace);

    Throwable getError(WebRequest webRequest);
}

// reactive ErrorAttributes
public interface ErrorAttributes {

    Map<String, Object> getErrorAttributes(ServerRequest request,
            boolean includeStackTrace);

    Throwable getError(ServerRequest request);

    void storeErrorInformation(Throwable error, ServerWebExchange exchange);

}

이 인터페이스를 통해 아래와 같은 구현체가 제공된다.

  제공되는 구현체 autoConfiguration 설정 구현체 사용 대상
servlet (webmvc) org.springframework.boot.web.servlet.error.DefaultErrorAttributes ErrorMvcAutoConfiguration BasicErrorController
reacitve (webflux) org.springframework.boot.web.reactive.error.DefaultErrorAttributes ErrorWebFluxAutoConfiguration DefaultErrorWebExceptionHandler

Spring boot는 에러가 발생하면 각각 사용 대상에서 ErrorAttributes를 사용하여 에러를 처리하게 된다.

따라서 Spring boot를 사용하는 경우 별도의 Exception Controller를 등록하지 않고 ErrorAttributes만 구현하면 에러 처리가 가능하다.

DefaultErrorAttributes 사용

스프링이 제공하는 DefaultErrorAttributes는 아래 항목들을 json이나 error view 페이지에 제공한다.

  • timestamp - 에러 발생 시간
  • status - 상태 코드
  • error - 에러 이유
  • exception - 에러 class 이름
  • message - 에러 메시지
  • errors - BindingResult 에러의 경우 ObjectErors
  • trace - exception stack trace
  • path - 에러 발생 url

json으로 응답되는 결과 페이지는 대략 아래와 같은 형태이다.

HTTP/1.1 500 Internal Server Error
{
    "timestamp": 1412685688268,
    "status": 500,
    "error": "Internal Server Error",
    "exception": "com.example.CustomException",
    "message": null,
    "path": "/example"
}

Custom ErrorAttributes 만들기

boot에서는 Servlet과 Reactive를 되도록 유사한 형태로 사용할 수 있도록 ErrorAttributes interface를 제공하기 때문에 아래 예제를 interface만 servlet/reactive 구별하여 상속하면 된다.

ErrorAttributes를 별도 구현하면 Spring boot는 DefaultErrorAttributes 대신 해당 ErrorAttributes를 사용하게 된다.

아래와 같이 만들었다고 가정하자.

@Component
public class TestErrorAttributes implements ErrorAttributes {
    private static final String ERROR_ATTRIBUTE = TestErrorAttributes.class.getName() + ".ERROR";

    @Override
    public Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
        Map<String, Object> errorAttributes = new LinkedHashMap<>();
        errorAttributes.put("timestamp", new Date());
        errorAttributes.put("path", request.path());
        errorAttributes.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
        return errorAttributes;
    }

    @Override
    public Throwable getError(ServerRequest request) {
        return (Throwable) request.attribute(ERROR_ATTRIBUTE)
                .orElseThrow(() -> new IllegalStateException(
                        "Missing exception attribute in ServerWebExchange"));
    }

    // storeErrorInfomation은 reactive에서만 구현하는 method
    @Override
    public void storeErrorInformation(Throwable error, ServerWebExchange exchange) {
        exchange.getAttributes().putIfAbsent(ERROR_ATTRIBUTE, error);
    }
	
}

위와 같이 custom ErrorAttributes를 선언하면 아래처럼 에러 결과가 변경된다.

HTTP/1.1 500 Internal Server Error
{
    "timestamp": 1412685688268,
    "path": "/example"
}

원하는 대로 에러 응답 결과를 바꿀 수 있다.

주의해야 할 점은 errorAttributes에 status란 키 값은 필수로 저장해야 한다.

해당 값을 기준으로 status를 처리하고 응답을 하게 된다.

어떤 에러 처리를 사용해야 할까?

Spring Boot가 제공하는 ErrorAttributes는 단일 구현으로 에러를 처리한다.

그러다 보니 모듈별로 Exception을 상속해서 별도 정의하는 경우처럼 다양한 에러에 대한 대응을 이 하나의 구현으로 처리하는 것은 많은 무리가 따른다.

Spring Framework이 제공하는 global exception handler 방식은 다양한 exception에 대해 별도 정의를 통한 관리가 가능하고 에러 응답 메시지를 별도 구현이 가능하지만 그로 인해 복잡도가 있고 spring boot의 error attribute와 별도의 제어가 돼버려서 spring boot 내부적인 공통화 처리에서 별도의 동작으로 분리되어 파생되는 문제들이 생길 수 있다.

개인적으로 현재는 global exception handler를 통한 공통화 에러 메시지를 별도 구현하여 응답하는 형태를 사용하고 있다. 

반응형
profile

파란하늘의 지식창고

@Bluesky_

도움이 되었다면 광고를 클릭해주세요