본문 바로가기
코딩 공부/web & Java

[Spring] 전역 예외 처리

by 현장 2023. 11. 4.

스프링의 기본적인 예외 처리

Spring은 만들어질 때부터 에러 처리를 위한 BasicErrorController를 구현해 두었고, 스프링 부트는 예외가 발생하면 기본적으로 /error로 에러 요청을 다시 전달하도록 WAS 설정을 해두었다. 그래서 별도의 설정이 없다면 예외 발생 시에 BasicErrorController로 에러 처리 요청이 전달됩니다.

참고로 이는 스프링 부트의 WebMvcAutoConfiguration를 통해 자동 설정이 되는 WAS의 설정입니다.

 

전역 예외 처리

- ControllerAdvice

Spring은 전역적으로 ExceptionHandler를 적용할 수 있는 @ControllerAdvice와 @RestControllerAdvice 어노테이션을 제공하고 있습니다.

@ControllerAdivce는 여러 컨트롤러에 대해 전역적으로 ExceptionHandler를 적용해 주므로  에러를 핸들링하는 클래스를 만들어 어노테이션을 붙여주면 에러 처리를 위임할 수 있습니다.

만약 특정 클래스에만 제한적으로 적용하고 싶다면, @RestControllerAdvice의 basePackages 등을 설정함으로써 제한할 수 있습니다.
2종류의 ControllerAdivce의 차이점은 @RestControllerAdvice는 @ControllerAdvice에 @ResponseBody가 붙어 있어 응답을 Json으로 내려줍니다.

 

ControllerAdvice를 사용함으로써 얻을 수 있는 이점

  • 하나의 클래스로 모든 컨트롤러에 대해 전역적으로 예외 처리가 가능합니다.
  • 직접 정의한 에러 응답을 일관성 있게 클라이언트에게 내려줄 수 있습니다.
  • 별도의 try-catch문이 없어 코드의 가독성이 높아집니다.


ControllerAdvice 사용시 주의해야 할 점

  • 한 프로젝트당 하나의 ControllerAdivce만 관리하는 것이 좋습니다.
  • 만약 여러 ControllerAdvice가 필요하다면 basePackages나 annotations 등을 지정해야 합니다.
  • 직접 구현한 Exception 클래스들은 한 공간에서 관리합니다.

 

- ResponseEntityExceptionHandler 추상 클래스

Spring은 스프링 예외를 미리 처리해둔 ResponseEntityExceptionHandler를 추상 클래스로 제공하고 있습니다. ResponseEntityExceptionHandler에는 스프링 예외에 대한 ExceptionHandler가 모두 구현되어 있으므로 @ControllerAdvice 클래스가 이를 상속받게 하면 됩니다.
이 추상 클래스를 상속받지 않는다면 스프링 예외들은 DefaultHandlerExceptionResolver가 처리하게 되는데, 그러면 예외 처리기가 달라지므로 클라이언트가 일관되지 못한 에러 응답을 받지 못하게 되어서 되도록이면 ResponseEntityExceptionHandler를 상속시키는 것이 좋습니다.
또한 이는 기본적으로 에러 메세지를 반환하지 않으므로, 스프링 예외에 대한 에러 응답을 보내려면 클래스에

handleExceptionInternal 메서드를 오버라이딩 해야 합니다.

-예시

@ControllerAdvice // 전역적으로 적용
public class CustomizedResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(Exception.class)
    public final ResponseEntity<ErrorDetails> handleAllException(Exception ex, WebRequest request) throws Exception {
        ErrorDetails errorDetails = new ErrorDetails(
                LocalDateTime.now(),
                ex.getMessage(),
                request.getDescription(false)
        );

        return new ResponseEntity<ErrorDetails>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR);
    }

    @ExceptionHandler(UserNotFoundException.class)
    public final ResponseEntity<ErrorDetails> handleUserNotFoundException(Exception ex, WebRequest request) throws Exception {
        ErrorDetails errorDetails = new ErrorDetails(
                LocalDateTime.now(),
                ex.getMessage(),
                request.getDescription(false)
        );

        return new ResponseEntity<ErrorDetails>(errorDetails, HttpStatus.NOT_FOUND);
    }
}

 

public class ErrorDetails {
    // 시간 정보
    private LocalDateTime timestamp;
    // 메세지
    private String message;
    // 상세 정보
    private String datails;

    public ErrorDetails(LocalDateTime timestamp, String message, String datails) {
        this.timestamp = timestamp;
        this.message = message;
        this.datails = datails;
    }

    public LocalDateTime getTimestamp() {
        return timestamp;
    }

    public String getMessage() {
        return message;
    }

    public String getDatails() {
        return datails;
    }
}

결과

- handleExceptionInternal 메서드

ResponseEntityExceptionHandler의 handleExceptionInternal() 메소드를 오버라이딩하여 응답을 커스터마이징할 수 있습니다.

 

- 예외 처리 흐름

1. ExceptionHandlerExceptionResolver: 에러 응답을 위한 Controller나 ControllerAdvice에 있는 ExceptionHandler를 처리합니다.

ㄱ.  예외가 발생한 컨트롤러 안에 적합한 @ExceptionHandler가 있는지 검사합니다.
ㄴ. 컨트롤러의 @ExceptionHandler에서 처리가 가능하다면 처리하고, 그렇지 않으면 ControllerAdivce로 넘어갑니다.
ㄷ. ControllerAdvice 안에 적합한 @ExceptionHandler가 있는지 검사하고, 없으면 다음 처리기로 넘어갑니다.

 

2. ResponseStatusExceptionResolver: Http 상태 코드를 지정하는 @ResponseStatus 또는 ResponseStatusException를 처리합니다.

ㄱ. @ResponseStatus가 있는지 또는 ResponseStatusException인지 검사합니다.
ㄴ. 맞으면 ServletResponse의 sendError()로 예외를 서블릿까지 전달하고, 서블릿이  BasicErrorController로 요청을 전달합니다.
ㄷ. DefaultHandlerExceptionResolver:  스프링 내부의 기본 예외들을 처리합니다.

 

3. Spring의 내부 예외인지 검사하여, 맞으면 에러를 처리하고 아니면 넘어갑니다.

ㄱ. 적합한 ExceptionResolver가 없으므로 예외가 서블릿까지 전달되고, 서블릿은 SpringBoot가 진행한 자동 설정에 맞게 BasicErrorController로 요청을 다시 전달합니다.

 

Reference