Spring 전역 예외 처리: @RestControllerAdivce 적용

u-nij·2022년 10월 13일
0
post-thumbnail

전역 예외 처리

ControllerAdvice

  • Spring은 전역적으로 ExceptionHandler를 적용할 수 있는 @ControllerAdvice@RestControllerAdvice 어노테이션을 제공하고 있다.
  • @ControllerAdivce는 여러 컨트롤러에 대해 전역적으로 ExceptionHandler를 적용해준다. 다음과 같이 에러를 핸들링하는 클래스를 만들어 어노테이션을 붙여주면 에러 처리를 위임할 수 있다.
    • 만약 특정 클래스에만 제한적으로 적용하고 싶다면, @RestControllerAdvice의 basePackages 등을 설정함으로써 제한할 수 있다
  • @RestControllerAdvice@ControllerAdvice와 달리 @ResponseBody가 붙어 있어 응답을 Json으로 내려준다.

ResponseEntityExceptionHandler 추상 클래스

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

적용 예시

@RestControllerAdvice // 전역 예외 처리
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { // 추상 클래스 상속

	// NotFoundAccountException 발생시 에러 처리
    @ExceptionHandler(NotFoundAccountException.class)
    public ResponseEntity<?> handleNotFoundEntity(NotFoundException e) {
        return handleExceptionInternal(e.getExceptionCode());
    }
}

handleExceptionInternal 메소드

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

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

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

ControllerAdvice 사용시 주의해야 할 점

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

예외 처리 흐름

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

    1. 예외가 발생한 컨트롤러 안에 적합한 @ExceptionHandler가 있는지 검사한다.
    2. 컨트롤러의 @ExceptionHandler에서 처리가 가능하다면 처리하고, 그렇지 않으면 ControllerAdivce로 넘어간다.
    3. ControllerAdvice 안에 적합한 @ExceptionHandler가 있는지 검사하고, 없으면 다음 처리기로 넘어간다.
  2. ResponseStatusExceptionResolver: Http 상태 코드를 지정하는 @ResponseStatus 또는 ResponseStatusException를 처리함

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

    1. Spring의 내부 예외인지 검사하여, 맞으면 에러를 처리하고 아니면 넘어간다.
  4. 적합한 ExceptionResolver가 없으므로 예외가 서블릿까지 전달되고, 서블릿은 SpringBoot가 진행한 자동 설정에 맞게 BasicErrorController로 요청을 다시 전달한다.

@RestControllerAdvice 적용

  • View를 사용하지 않고 Rest API로만 사용하기 때문에 @RestControllerAdvice를 사용하겠다.

예외 케이스 관리

  • 클라이언트에게 보내줄 에러 코드를 한 곳에서 관리할 수 있다.
  • 에러 이름과 HTTP 상태 및 메세지를 가지고 있는 Enum Class이다.
@Getter
@RequiredArgsConstructor
public enum ErrorCode {

    /*
     * 400 BAD_REQUEST: 잘못된 요청
     */
    BAD_REQUEST(HttpStatus.BAD_REQUEST, "Invalid request."),

    /*
     * 401 UNAUTHORIZED: 인증되지 않은 사용자의 요청
     */
    UNAUTHORIZED_REQUEST(HttpStatus.UNAUTHORIZED, "Unauthorized."),

    /*
     * 403 FORBIDDEN: 권한이 없는 사용자의 요청
     */
    FORBIDDEN_ACCESS(HttpStatus.FORBIDDEN, "Forbidden."),

    /*
     * 404 NOT_FOUND: 리소스를 찾을 수 없음
     */
    NOT_FOUND(HttpStatus.NOT_FOUND, "Not found."),

    /*
     * 405 METHOD_NOT_ALLOWED: 허용되지 않은 Request Method 호출
     */
    METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "Not allowed method."),

    /*
     * 500 INTERNAL_SERVER_ERROR: 내부 서버 오류
     */
    INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Server error.");


    private final HttpStatus httpStatus;
    private final String message;
}

에러 응답 형식 지정

  • 실제 사용자에게 JSON 형식으로 보여주기 위한 에러 응답 형식을 지정한다.
@Getter
public class ErrorResponse {
    private final LocalDateTime timestamp = LocalDateTime.now();
    private final int statusCode;
    private final String error;
    private final String message;

    public ErrorResponse(ErrorCode errorCode) {
        this.statusCode = errorCode.getHttpStatus().value();
        this.error = errorCode.getHttpStatus().name();
        this.message = errorCode.getMessage();
    }
}

에러 코드 정의

  • 발생한 예외를 처리해줄 에러 클래스(Exception Class)를 추가한다.
  • RuntimeException을 상속받아 언체크 예외로 활용한다.
@Getter
@RequiredArgsConstructor
public class RestApiException extends RuntimeException {
    private final ErrorCode errorCode;
}

전역 예외 처리

  • @ExceptionHandler: 발생한 특정 예외를 잡아 하나의 메소드에서 공통 처리할 수 있게 한다.
  • @RestControllerAdvice: 프로젝트 전역에서 발생하는 모든 예외를 잡아준다.
  • 모든 예외를 잡은 후에, Exception 종류별로 메소드를 공통 처리할 수 있다.
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    /*
     * Developer Custom Exception: 직접 정의한 RestApiException 에러 클래스에 대한 예외 처리
     */
    @ExceptionHandler(RestApiException.class)
    protected ResponseEntity<ErrorResponse> handleCustomException(RestApiException ex) {
        ErrorCode errorCode = ex.getErrorCode();
        return handleExceptionInternal(errorCode);
    };

	// handleExceptionInternal() 메소드를 오버라이딩해 응답 커스터마이징
    private ResponseEntity<ErrorResponse> handleExceptionInternal(ErrorCode errorCode) {
        return ResponseEntity
                .status(errorCode.getHttpStatus().value())
                .body(new ErrorResponse(errorCode));
    }
}

테스트

Controller에서 무조건 BAD_REQUSEST 응답을 내리는 코드를 작성해보았다.

@RestController
@RequestMapping("/api")
public class NaverLoginApiController {
    @GetMapping("/test")
    public ResponseEntity<?> test() {
        throw new RestApiException(ErrorCode.BAD_REQUEST);
    }
 }

Postman에서 GET "http://localhost:8080/api/test" 실행,

참고

https://mangkyu.tistory.com/204
https://bcp0109.tistory.com/303
https://velog.io/@evelyn82ny/Exception-handling-using-RestControllerAdvice

0개의 댓글