ExceptionHandler

Panda·2022년 3월 26일
0

Spring

목록 보기
15/42

기본적으로 스프링에서 예외가 발생할 시
BasicController에 이해서 에러가 처리 되기는 하지만
저희가 원하는 형태로 반환되는게 아니기 때문에

ExceptionHandler에 대해서 알아볼까 합니다.

HandlerExceptionResolver

스프링은 Java 언어를 사용해 예외 처리를 하기 위해서는 기본적으로 try-catch를 사용해야 하지만 try-catch를 모든 예외 상황에 붙이는 것은 코드의 가독성을 떨어뜨립니다. Spring은 이러한 문제를 해결하기 위해 에러 처리라는 공통 관심사(cross-cutting concerns)를 메인 로직으로부터 분리 해내는 다양한 예외 처리 방식을 고안하였으며, 이를 위해 예외를 처리하는 핸들러들을 추상화한 HandlerExceptionResolver 인터페이스를 만들었습니다.

public interface HandlerExceptionResolver {
    ModelAndView resolveException(HttpServletRequest request, 
            HttpServletResponse response, Object handler, Exception ex);
}

Spring은 다양한 예외 처리 방법을 제공하고 있는데 각각의 다양한HandlerExceptionResolver 인터페이스의 구현체들을 빈으로 등록해둠으로써 발생한 예외 방식에 맞는 ExceptionResolver가 예외 처리를 진행하도록 합니다.
Spring은 기본적으로 아래의 4가지 ExceptionResolver 구현체들을 빈으로 등록해둡니다.

  • DefaultErrorAttributes : 에러 속성을 저장하며 직접 예외를 처리하지는 않음

  • DefaultHandlerExceptionResolver : 스프링의 예외들을 처리

  • ResponseStatusExceptionResolver : @ResponseStatus 또는 ResponseStatusException에 의한 예외를 처리

  • ExceptionHandlerExceptionResolver : Controller나 ControllerAdvice에 있는 ExceptionHandler에 의한 예외를 처리

따라서 예외 처리 방식에는 밑에 4가지가 존재하는데

  • @ResponseStatus
  • ExceptionHandler
  • ControllerAdvice, RestControllerAdvice
  • ResponseStatusException

@ResponseStatus 같은 경우는 단점들이 있는데

  • 에러 응답의 내용(Payload)를 수정할 수 없음
  • 예외 상황마다 예외 클래스를 추가 필수
  • 예와 클래스와 강하게 결합되어 모든 예외에 대해 동일한 상태와 에러 메세지를 반환

좀 단점이 많이 독보적이긴 합니다 ㅋㅋㅋㅋㅋㅋㅋㅋㅋ

RestControllerAdvice 와 ExceptionHandler로 예외 처리를 구현해보자 합니다!!
(ExceptionHandler만 사용하면 Global한 예외처리를 못하기 때문)

ControllerAdvice

@ExceptionHandler의 한계를 극복한 전역적으로 예외 처리가 가능한
@ControllerAdvice, @RestControllerAdvice 제공하고 있습니다.
ControllerAdvice는 AOP 방식으로 예외가 처리되고 있습니다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ControllerAdvice
@ResponseBody
public @interface RestControllerAdvice {
    ...
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
    ...
}
@RestControllerAdvice
public class GlobalExceptionHandler {

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

        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse);
    }
}

이제 NoSuchElementFoundException 예외가 발생하게 되면 밑에와 같은 Response를 받습니다.

{
    "code": "NOT_FOUND",
    "message": "Item not found"
}

이런식으로 우리가 원하는 대로 payload를 변경 할 수 있게되었습니다.

특정 컨트롤러에만 제한적으로 쓰고 싶다면?

ControllerAdvice는 ProductController가 아니라 다른 컨트롤러들에도 동일하게 적용됩니다.(전역 예외 처리 이므로) 만약 특정 클래스에만 제한적으로 적용하고 싶다면 @RestControllerAdvice의 basePackages 등을 설정함으로써 제한할 수 있습니다.

Spring은 ControllerAdvice를 이용할 때 스프링 예외를 미리 처리해둔 ResponseEntityExceptionHandler를 추상 클래스로 제공하고 있습니다. ResponseEntityExceptionHandler에는 스프링 예외에 대한 ExceptionHandler가 모두 구현되어 있지만 에러 메세지는 반환하지 않아서 스프링 예외 발생 시에 에러 응답을 보내려면 아래 메소드를 오버라이딩 해주어야 합니다.

public abstract class ResponseEntityExceptionHandler {
    ...

    protected ResponseEntity<Object> handleExceptionInternal(
        Exception ex, @Nullable Object body, HttpHeaders headers, HttpStatus status, WebRequest request) {
            
        ...
    }
}

만약 이 추상 클래스를 상속받지 않는다면 예외들은 ModelAndView 객체를 반환하는 DefaultHandlerExceptionResolver로 리다이렉트됩니다!!!
그러므로 오류 응답을 반환하기 위해서는 ResponseEntityExceptionHandler를 상속받는 것이 좋습니다. 또한 해당 클래스를 상속받지 않으면 NoHandlerFoundException가 발생한 경우 별도의 처리가 되지 않을 것입니다.

ControllerAdvice를 이용함으로써 다음과 같은 장점들이 있습니다.

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

그래서 일반적으로는 ControllerAdvice 사용하는게 가장 좋다고 합니다.

ControllerAdvice 사용 시 주의할점

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

Spring 예외 처리 흐름

Spring에서 예외들은 순차적으로 Resolver들에 의해 처리가 됩니다. 또한 Resolver들은 디스패쳐 서블릿에 의해 빈으로 등록되어있습니다.
3가지 Resolver가 있습니다.

  • ExceptionHandlerExceptionResolver: Controller나 ControllerAdvice에 있는 ExceptionHandler를 처리함
  • ResponseStatusExceptionResolver: @ResponseStatus 또는 ResponseStatusException를 처리함
  • DefaultHandlerExceptionResolver: 스프링의 예외들들을 처리함
    ex) HttpMediaTypeNotSupportedException 등

예외 처리 순서

  1. 예외가 던져졌을 때 Spring은 먼저 예외가 발생한 컨트롤러 안에 적합한 @ExceptionHandler가 있는지 검사하고, 예외를 처리할 수 있다면 에러 처리
  2. 컨트롤러의 @ExceptionHandler에서 처리가 불가능하다면 ControllerAdvice를 찾고 적합한 @ExceptionHandler가 있는지 검사하고 예외를 처리할 수 있다면 에러 처리
  3. ControllerAdvice에서 처리가 불가능하면 @ResponseStatus가 있는지 검사하고, 있다면 ResponseStatusExceptionResolver에 의해 처리 되고, 없다면 DefaultHandlerExceptionResolver에 의해 처리

에러 응답을 직접 반환하지 않는다면 혹은 ExceptionHandler를 구현안했다면
BasicErrorController에 의해 거쳐서 반환이 됩니다.

느낀 점

예외 처리를 어떻게 해야되지 감이 1도 안잡혔었는데

ControllerAdvice로 전역 예외 처리를 해줘서
Exception 관련으로 한눈에 볼수 있고 처리를 할 수 있으니까 매우 매우 편한것 같습니다.
이게 AOP의 힘인가 ㅋㅋㅋㅋ

profile
실력있는 개발자가 되보자!

0개의 댓글