[Spring] 스프링 예외처리(2) - @ExceptionHandler, @ControllerAdvice 개념

ohahsis·2024년 5월 4일

SpringBoot

목록 보기
2/2
post-thumbnail

@ExceptionHandler

  • @ExceptionHandlerController 계층에서 발생하는 에러를 잡아서 메서드로 처리해주는 기능으로, 매우 유연하게 에러를 처리할 수 있는 방법을 제공한다.
  • Service, Repository에서 발생하는 에러는 제외한다.
  • @ExceptionHandler는 다음에 어노테이션을 추가함으로써 에러를 손쉽게 처리할 수 있다.
    • 컨트롤러의 메소드
    • @ControllerAdvice@RestControllerAdvice가 있는 클래스의 메소드

컨트롤러 메소드에 @ExceptionHandler 추가

@RestController
@RequiredArgsConstructor
public class ProductController {

  private final ProductService productService;
  
  @GetMapping("/product/{id}")
  public Response getProduct(@PathVariable String id){
    return productService.getProduct(id);
  }

  @ExceptionHandler(NoSuchElementFoundException.class)
  public ResponseEntity<String> handleNoSuchElementFoundException(NoSuchElementFoundException exception) {
    return ResponseEntity.status(HttpStatus.NOT_FOUND).body(exception.getMessage());
  }
}

코드 설명

  • @Controller로 선언된 클래스 안에서 @ExceptionHandler 어노테이션으로 메서드 안에서 발생할 수 있는 에러를 처리할 수 있다.
  • @ExceptionHandler에 의해 발생한 예외는 ExceptionHandlerExceptionResolver에 의해 처리가 된다.
  • @ExceptionHandler는 Exception 클래스들을 속성으로 받아 처리할 예외를 지정할 수 있다. 만약 @ExceptionHandler에 예외 클래스를 지정하지 않는다면, 파라미터에 설정된 에러 클래스를 처리하게 된다.

장점

  • ExceptionHandler@ResponseStatus와 달리 에러 응답(payload)을 자유롭게 다룰 수 있다는 점에서 유연하다. 예를 들어 응답을 다음과 같이 정의해서 내려준다면 좋을 것이다.
    • code: 어떠한 종류의 에러가 발생하는지에 대한 에러 코드
    • message: 왜 에러가 발생했는지에 대한 설명
    • errors: 어느 값이 잘못되어 @Valid에 의한 검증이 실패한 것인지를 위한 에러 목록

파라미터와 반환 타입
ExceptionHandler의 파라미터로 HttpServletRequest나 WebRequest 등을 얻을 수 있으며 반환 타입으로는 ResponseEntity, String, void 등 자유롭게 활용할 수 있다.
더 자세한 내용은 아래 공식 문서에서 확인 가능하다.

주의할 점

  • @ExceptionHandler를 사용 시에 주의할 점은 @ExceptionHandler에 등록된 예외 클래스파라미터로 받는 예외 클래스동일해야 한다는 것이다. 만약 값이 다르다면 스프링은 컴파일 시점에 에러를 내지 않다가 런타임 시점에 다음과 같은 에러를 발생시킨다.
java.lang.IllegalStateException: No suitable resolver for argument [0] [type=...]
HandlerMethod details: ...

@ExceptionHandler는 컨트롤러에 구현하므로 특정 컨트롤러에서만 발생하는 예외만 처리된다. 하지만 컨트롤러에 에러 처리 코드가 섞이며, 에러 처리 코드가 중복될 가능성이 높다. 그래서 스프링은 전역적으로 예외를 처리할 수 있는 좋은 기술을 제공해준다.


@ControllerAdvice

ControllerAdvice는 여러 컨트롤러에 대해 전역적으로 ExceptionHandler를 적용해준다. 위에서 보이듯 ControllerAdvice 어노테이션에는 @Component 어노테이션이 있어서 ControllerAdvice가 선언된 클래스는 스프링 빈으로 등록된다.
그러므로 다음과 같이 전역적으로 에러를 핸들링하는 클래스를 만들어 어노테이션을 붙여주면 에러 처리를 위임할 수 있다.

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(NoSuchElementFoundException.class)
    protected ResponseEntity<?> handleNoSuchElementFoundException(NoSuchElementFoundException e) {
        final ErrorResponse errorResponse = ErrorResponse.builder()
                .code("Item Not Found")
                .message(e.getMessage()).build();

        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse);
    }
}
  • @ControllerAdvice@Controllerhandler에서 발생하는 에러들을 모두 잡아준다.
  • @ControllerAdvice안에서 @ExceptionHandler를 사용하여 에러를 잡을 수 있다.

@ControllerAdvice 이점

@ControllerAdvice를 이용하면 다음과 같은 이점을 누릴 수 있다.

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

@ControllerAdvice 사용시 주의점

일관된 예외 응답을 위해서는 다음의 사항을 주의해야 한다.

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

📍 스프링의 예외 처리 흐름

다음과 같은 예외 처리기들은 스프링의 빈으로 등록되어 있고, 예외가 발생하면 순차적으로 다음의 Resolver들이 처리가능한지 판별한 후에 예외가 처리된다.

  1. ExceptionHandlerExceptionResolver 동작

    • 예외가 발생한 컨트롤러 안에 적합한 @ExceptionHandler가 있는지 검사한다.
    • 컨트롤러의 @ExceptionHandler에서 처리가능하다면 처리하고, 그렇지 않으면 ControllerAdvice로 넘어간다.
    • ControllerAdvice안에 적합한 @ExceptionHandler가 있는지 검사하고 없으면 다음 처리기로 넘어간다.
  2. ResponseStatusExceptionResolver 동작

    • @ResponseStatus가 있는지 또는 ResponseStatusException인지 검사한다.
    • 맞으면 ServletResponsesendError()로 예외를 서블릿까지 전달되고, 서블릿이 BasicErrorController로 요청을 전달한다.
  3. DefaultHandlerExceptionResolver 동작

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


참고 자료

profile
백엔드 개발자입니다.

0개의 댓글