쟈미로그

[Spring] Spring의 예외 처리 구조 본문

Spring Boot

[Spring] Spring의 예외 처리 구조

쟈미 2023. 3. 23. 19:02

서론

평소에 전역으로 예외를 처리해서 반환해줄 일이 있을 때면 @ControllerAdvice와 @HandlerException을 사용하곤했다.

많은 레퍼런스에서 이 방식을 사용하길래 쓰게 됐고, 자세한 동작원리는 모른 채 사용했다. 그렇게 예외처리 == @ControllerAdvice + @HandlerException을 사용하는 것 이라는 생각을 갖고 개발하다가 실수를 하게됐다. 이미 응답을 준 상황에서도 예외를 던지면 ControllerAdvice의 HandlerException가 잡아서 처리해줄 거라는 착각을 한 것..!

 

두 어노테이션의 동작 방식을 몰랐고, "예외 처리"를 왜 하는가에 대한 개념도 모호해서 이런 일이 발생했던 것 같다. (예외가 발생했을 때 왜 잡을까? 잡아서 클라이언트에게 보여주는 이유 등)

그래서 이참에 스프링의 예외 처리 구조를 알아보고, 예외처리에 대해 바뀐 내 생각도 정리해보고자한다!

 

 

스프링의 예외처리 방법

스프링은 예외를 효과적으로 다룰 수 있도록 다양한 방법을 제공한다고 한다.

예외 발생 시 예외를 잡고 커스텀하게 반환해주기 위해서, 스프링엔 예외처리 전략을 추상화한 HandlerExceptionResolver 인터페이스가 만들어져있다. 

public interface HandlerExceptionResolver {

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

}

(주의할 점은 파라미터의 handler에 들어오는 값이 예외가 발생한 컨트롤러 객체라는 점이다. 그래서 HandlerExceptionResolver는 컨트롤러에서 발생하는 예외만을 처리할 수 있다.)

 

컨트롤러에서 예외가 발생하면 그 예외는 디스패쳐 서블릿까지 던져지는데, HandlerExceptionResolver의 구현체들 중에서 적용 가능한 구현체에 맞춰서 예외가 처리돼서 응답으로 내려진다.

HandlerExceptionResolver 구현체의 종류는 총 4개다.

  1. ExceptionHandlerExceptionResolver
  2. ResponseStatusExceptionResolver
  3. DefaultHandlerExceptionResolver
  4. DefaultErrorAttributes

여기서 DefaultErrorAttributes는 직접 예외를 처리하진 않고 속성만 관리하는 것이라서 다른 구현체들과 성격이 다르다고 한다. 그래서 나머지 1, 2, 3 구현체들 위주로 알아보자. (예외 발생 시 처리되는 구현체의 우선순위는 위 목록 1, 2, 3번 순서대로 실행된다.)

 

 

1. ExceptionHandlerExceptionResolver

@ExceptionHandler 어노테이션을 사용해서 예외를 처리하면 ExceptionHandlerExceptionResolver가 동작한다.

ExceptionHandler는 

  • @Controller/@RestController 클래스의 메소드
  • @ControllerAdvice/@RestControllerAdvice 클래스의 메소드

에 붙여서 사용할 수 있다.

 

@RestController
public class ProductController {

    ...

    @ExceptionHandler(NotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFoundException(NotFoundException exception) {
        ...
    }
}

(컨트롤러에 사용하는 예시)

 

특정 컨트롤러에서 ExceptionHandler 메소드를 정의하면 해당 컨트롤러에서만 그 ExceptionHandler가 동작한다. 그래서 전역으로 예외를 처리하고싶다면 컨트롤러 어드바이스를 사용하는 것이 더 편하다.

 

 

2. ResponseStatusExceptionResolver

ResponseStatusExceptionResolver은 @ResponseStatus, ResponseStatusException를 사용하면 동작한다.

 

2-1. @ResponseStatus

@ResponseStatus는 예외의 HTTP 상태코드를 변경해주는 어노테이션이다.

ResponseStatus는

  • Exception 클래스 자체
  • @ExceptionHandler와 함께 메소드
  • @ControllerAdvice/@RestControllerAdvice와 함께 클래스

에 붙여 사용할 수 있다.

 

@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class CustomNotFoundException extends RuntimeException {
   ...
}

(Exception 클래스 자체에 사용하는 예시)

 

이렇게 사용할 경우 아래와 같은 응답을 받을 수 있는데, 이는 문제가 있다.

{
    "timestamp": "2023-04-04T14:26:45.146+00:00",
    "status": 404,
    "error": "Not Found",
    "path": "/test"
}

이 응답은 BasicErrorController에 의한 것으로 (ex. WhiteLabel 에러 페이지..)

결국 디스패쳐 서블릿에서 ResponseStatusExceptionResolver가 동작해도 WAS의 에러처리 동작을 하는 것이다. (자세한건 BasicErrorController를 찾아보자)

이처럼 @ResponseStatus는

  • WAS의 복잡한 에러처리 요청을 거치는 것 (BasicErrorController)
  • 응답 내용을 변경하기 까다롭다는 것
  • 상태코드를 커스텀하게 쓰기 불편함

등에서 사용하기 불편한 점들이 많다. 

 

2-2. ResponseStatusException

@StatusException는 알아봤듯이 응답을 커스텀하게 쓰기엔 불편하다.

그래서 Spring5에선 상태코드 외에도 reason, cause도 추가할 수 있는 ResponseStatusException이 추가됐다.

 

이렇게 사용할 수 있다.

...
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Custom Not Found!!!");
...

하지만 결국 WAS 에러처리 요청을 거친다는 점과, 에러응답을 완전히 제어할 수는 없다는 점에서 @ExceptionHandler보다 한계가 있다.

 

 

3. DefaultHandlerExceptionResolver

DefaultHandlerExceptionResolver는 스프링에서 발생하는 주요 예외를 처리해서 반환해주는 기본 리졸버다. 가장 마지막 우선순위로 쓰이는 리졸버다.

 

예외 발생 시 이 리졸버가 동작했다는 것은 해당 예외가 따로 처리되지 않았다는 것으로, @HandlerException으로 커스텀하게 예외를 반환하고 있다면 이 리졸버는 최대한 동작하지 않도록 하는 것이 좋은 것 같다. (예외 응답이 일관적이지 않을테니까)

 

그렇게 하기 위해선 많은 예외들을 @ControllerAdvice안의 @HandlerException으로 잘 처리해야하는데 번거로움을 느낄 수 있다.

다행히 마침 스프링은 기본적인 예외를 미리 처리해둔 ResponseEntityExceptionHandler라는 추상클래스를 제공하고 있다. 그래서 @ControllerAdvice 클래스가 해당 추상클래스를 상속받도록 한다면, 많은 예외들을 일관적인 응답으로 내릴 수 있게된다!

 

 

 

 

 

 

 

참고

https://mangkyu.tistory.com/204
https://jaehun2841.github.io/2018/08/30/2018-08-25-spring-mvc-handle-exception/#Controller-%EB%A0%88%EB%B2%A8%EC%97%90%EC%84%9C%EC%9D%98-%EC%B2%98%EB%A6%AC

 

'Spring Boot' 카테고리의 다른 글

[Spring] 스프링의 AOP  (0) 2023.06.19
Comments