[Spring] 스프링 예외처리(1) - HandlerExceptionResolver

ohahsis·2024년 5월 3일

SpringBoot

목록 보기
1/2
post-thumbnail

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

스프링의 예외 처리 과정

우리가 개발한 Controller에서 발생한 예외를 Spring은 기본적으로 어떻게 처리하고 있는지 살펴보도록 하자.
스프링 부트에서 예외가 전달되는 과정은 크게 세 가지로 나뉜다.

1. WAS(톰캣)는 컨트롤러에 요청을 전달한다.
WAS(톰캣) -> 필터 -> 서블릿(디스패처 서블릿) -> 인터셉터 -> 컨트롤러

2. 컨트롤러 하위에서 예외 발생 후, 별도의 예외 처리가 없으면 WAS까지 에러가 전달된다.
컨트롤러(예외발생) -> 인터셉터 -> 서블릿(디스패처 서블릿) -> 필터 -> WAS(톰캣)

3. WAS는 애플리케이션에서 처리하지 못하는 예외라서 exception이 올라왔다 판단하고, 대응 작업으로 스프링부트가 등록한 에러 설정(/error)에 맞게 요청을 전달한다.
WAS(톰캣) -> 필터 -> 서블릿(디스패처 서블릿) -> 인터셉터 -> 컨트롤러(BasicErrorController)

총 정리하면 다음과 같다.
WAS(톰캣) -> 필터 -> 서블릿(디스패처 서블릿) -> 인터셉터 -> 컨트롤러 -> 컨트롤러(예외발생) -> 인터셉터 -> 서블릿(디스패처 서블릿) -> 필터 -> WAS(톰캣) -> WAS(톰캣) -> 필터 -> 서블릿(디스패처 서블릿) -> 인터셉터 -> 컨트롤러(BasicErrorController)

스프링의 기본적인 에러 처리 방식은 결국 에러 컨트롤러를 한 번 더 호출하는 것인데, 동작 과정을 보면 딱 봐도 복잡하고 반복적이다. 이를 제어하기 위해서는 별도의 설정이 필요하다.

서블릿은 dispatcherType으로 요청의 종류를 구분하는데, 일반적인 요청은 REQUEST이며 에러 처리 요청은 ERROR이다.

  • 서블릿 기술인 필터 등록(FilterRegistrationBean)을 할 때 호출될 dispatcherType타입을 설정할 수 있고, 별도의 설정이 없다면 REQUEST일 경우에만 필터가 호출된다.
  • 그러나 스프링 기술인 인터셉터는 dispatcherType을 설정할 수 없어 URI 패턴으로 처리가 필요하다.

스프링 부트에서는 WAS까지 직접 제어하게 되면서 이러한 WAS의 에러 설정까지 가능해졌다. 또한 이는 요청이 2번 생기는 것은 아니라, 1번의 요청이 2번 전달되는 것이다. 따라서 클라이언트는 이러한 에러 처리 작업이 진행되었는지 알 수 없다.

BasicErrorController의 동작 및 에러 속성들

BasicErrorControlleraccept 헤더에 따라 에러 페이지를 반환하거나 에러 메세지을 반환한다. 에러 경로는 기본적으로 /error로 정의되어 있으며 properties에서 server.error.path로 변경할 수 있다.

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {

    private final ErrorProperties errorProperties;
    ...

    @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
        ...
    }

    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        ...
        return new ResponseEntity<>(body, status);
    }
    
    ...
}

errorHtml()error()는 모두 getErrorAttributeOptions를 호출해 반환할 에러 속성을 얻는데, 기본적으로 DefaultErrorAttributes로부터 반환할 정보를 가져온다. DefaultErrorAttributes는 전체 항목들에서 설정에 맞게 불필요한 속성들을 제거한다.

  • timestamp: 에러가 발생한 시간
  • status: 에러의 Http 상태
  • error: 에러 코드
  • path: 에러가 발생한 uri
  • exception: 최상위 예외 클래스의 이름(설정 필요)
  • message: 에러에 대한 내용(설정 필요)
  • errors: BindingExecption에 의해 생긴 에러 목록(설정 필요)
  • trace: 에러 스택 트레이스(설정 필요)

클라이언트에서 기본 설정으로 받는 에러 응답

{
    "timestamp": "2021-12-31T03:35:44.675+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "path": "/product/5000"
}

이는 나름 잘 갖추어져 있지만 클라이언트 입장에서 유용하지 못하다. 클라이언트는 “Item with id 5000 not found”라는 메세지와 함께 404 status로 에러 응답을 받으면 훨씬 유용할 것이다.

server.error.include-message: always
server.error.include-binding-errors: always
server.error.include-stacktrace: always
server.error.include-exception: false

위와 같이 properties를 통해 에러 응답을 조정할 수 있다. 물론 운영 환경에서 구현이 노출되는 trace는 제공하지 않는 것이 좋다.
참고로 SpringBoot 2.3 이전에는 message를 기본적으로 제공하고 있었지만, SpringBoot 2.3부터는 클라이언트에게 너무 많은 정보가 노출되는 것을 방지하기 위해 기본적으로 제공하지 않게 되었다.

{
    "timestamp": "2021-12-31T03:35:44.675+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "trace": "java.util.NoSuchElementException: No value present ...",
    "message": "No value present",
    "path": "/product/5000"
}

하지만 설정을 변경했음에도 불구하고 status는 여전히 500이며, 유의미한 에러 응답을 전달하지 못한다. (참고로 여기서 status가 500인 이유는 에러가 처리되지 않고 WAS가 에러를 전달받았기 때문이다.) 또한 흔히 사용되는 API 에러 처리 응답으로는 보다 세밀한 제어가 요구된다.
이어서 상황에 맞는 에러 응답을 제공하기 위해 별도의 에러 처리 전략을 알아보자.


📍 스프링이 제공하는 다양한 예외처리 방법

HandlerExceptionResolver

Java에서는 예외 처리를 위해 try-catch를 사용해야 하지만, try-catch를 모든 코드에 붙이는 것은 비효율적이다. Spring은 에러 처리라는 공통 관심사(cross-cutting concerns)를 메인 로직으로부터 분리하는 다양한 예외 처리 방식을 고안하였고, 예외 처리 전략을 추상화한 HandlerExceptionResolver 인터페이스를 만들었다. (전략 패턴이 사용된 것이다.)

대부분의 HandlerExceptionResolver는 발생한 Exception을 catch하고 HTTP 상태응답 메세지 등을 설정한다. 그래서 WAS 입장에서는 해당 요청이 정상적인 응답인 것으로 인식되며, 위에서 설명한 복잡한 WAS의 에러 전달이 진행되지 않는다.

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

위의 Object 타입handler는 예외가 발생한 컨트롤러 객체이다. 예외가 던져지면 디스패처 서블릿까지 전달되는데, 적합한 예외 처리를 위해 HandlerExceptionResolver 구현체들을 빈으로 등록해서 관리한다. 그리고 적용 가능한 구현체를 찾아 예외 처리를 하는데, 우선순위대로 아래의 4가지 구현체들이 빈으로 등록되어 있다.

  • DefaultErrorAttributes: 에러 속성을 저장하며 직접 예외를 처리하지는 않는다.
  • ExceptionHandlerExceptionResolver: 에러 응답을 위한 Controller나 ControllerAdvice에 있는 ExceptionHandler를 처리함
  • ResponseStatusExceptionResolver: Http 상태 코드를 지정하는 @ResponseStatus 또는 ResponseStatusException를 처리함
  • DefaultHandlerExceptionResolver:  스프링 내부의 기본 예외들을 처리한다.

Spring은 아래와 같은 도구들로 ExceptionResolver를 동작시켜 에러를 처리할 수 있다.

  • ResponseStatus
  • ResponseStatusException
  • ExceptionHandler
  • ControllerAdvice, RestControllerAdvice

이 중 API 에러 처리를 위해서는 보편적으로 @ExceptionHandler를 사용한다. 다음 문서에서 @ExceptionHandler와 그 사용 방식에 대해 더 자세히 알아보자.


참고 자료

profile
백엔드 개발자입니다.

0개의 댓글