스프링 부트 예외처리 흐름 이해

Jang990·2023년 4월 14일
0

예외가 발생하면 다음과 같은 동작으로 처리된다.(WAS = 서블릿 컨테이너)

요청 -> WAS -> 필터 -> 인터셉터 -> 컨트롤러(오류 발생!) -> 인터셉터 -> 필터 
-> WAS에서 오류 감지 -> WAS에서 에러 처리 재요청 
-> 필터 -> 인터셉터 -> 컨트롤러 -> 인터셉터 -> 필터 -> WAS -> 응답

중요한 것은 클라이언트는 이 과정을 모르고 그냥 에러 페이지를 응답 받는 것이다.

예외 발생시 로그 확인

HttpServletResponse에서 제공하는 sendError()를 활용해서 서블릿 컨테이너에게 오류가 발생했다는 점을 전달할 수 있다.
이것을 호출한다고 당장 예외가 발생하는 것은 아니다.
정상적으로 동작하지만 WAS에 도달해서 WAS가 오류를 감지하고 오류를 처리한다.

sendError()와 필터, 인터셉터를 이용해서 에러 발생시 어떤 흐름으로 동작되는지 확인해보자.

참고로 throw new Exception으로 예외를 발생시키면 WAS에 500을 전달한다.

예외 발생 컨트롤러

간단한 오류를 발생시키는 컨트롤러를 작성해보자.

@Controller
public class ServletExController {

    @GetMapping("/error-404")
    public void error404(HttpServletResponse response) throws IOException {
        response.sendError(HttpServletResponse.SC_NOT_FOUND, "찾지 못함");
    }
    @GetMapping("/error-500")
    public void error500(HttpServletResponse response) throws IOException {
        response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
    }
}

필터 및 인터셉터 등록

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor())
                .addPathPatterns("/**")
    }

    @Bean
    public FilterRegistrationBean<Filter> filterRegistrationBean() {
        FilterRegistrationBean<Filter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new LogFilter());
        registrationBean.addUrlPatterns("/*");
        return registrationBean;
    }
}

로그를 찍는 필터와 인터셉터를 미리 추가해두었다.
간단한 로그를 찍는 필터와 인터셉터이기 때문에 코드는 첨부하지 않겠다.

요청 로그 확인

/error-404로 요청을 보내고 로그를 확인해보자.

그런데 이상한 부분이 있다. /error-404 요청을 보내고 WAS에서 /error라는 URI로 재요청을 보내는데 필터를 거치지 않았다.

필터를 거치지 않는 이유

서버 내부에서 오류 페이지를 호출한다고 해서 해당 필터나 인터셉트가 한 번 더 호출되는 것은 매우 비효율적이다. 서블릿은 이러한 문제를 해결하기 위해서 DispatcherType을 제공한다.

다시 필터를 등록하는 코드로 돌아가보자.

	@Bean
    public FilterRegistrationBean<Filter> filterRegistrationBean() {
        FilterRegistrationBean<Filter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new LogFilter());
        registrationBean.addUrlPatterns("/*");
        registrationBean.setOrder(1);
        return registrationBean;
    }

해당 코드처럼 아무런 설정도 없으면 기본값은 DispatcherType.REQUEST이다.
만약 필터를 오류페이지를 요청할 때도 나오게 하고 싶다면 다음과 같이 바꿀 수 있다.

	@Bean
    public FilterRegistrationBean<Filter> filterRegistrationBean() {
        FilterRegistrationBean<Filter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new LogFilter());
        registrationBean.addUrlPatterns("/*");
        registrationBean.setOrder(1);
		registrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
        return registrationBean;
    }

하지만 따로 에러를 처리하기 위한 필터가 아니라면 기본값으로 설정하는 것이 제일 좋다.

이제 다시 로그를 확인해보자.

/error 요청에 대한 필터도 정상적으로 나오는 것을 확인할 수 있다.

인터셉터를 거치지 않게 하는 법

인터셉터의 경우 간단하다. excludePathPatterns에 해당 URI를 추가해 주면 된다. 그러면 해당 URI를 요청하면 인터셉터를 거치지 않는다.

	@Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/error") // 추가
    }

BasicErrorController

로그를 보면 예외 발생 후에 /error 페이지를 다시 요청하는 것을 확인할 수 있다.
/error페이지는 스프링 부트에서 제공해주는 BasicErrorController에서 처리한다.

BasicErrorController/error폴더 아래 위치한 상태코드를 출력해준다.
예를들어 다음과 같은 것들이 출력되는 것이다.

/error/500      - 500 에러일 때 출력
/error/400      - 400 에러일 때 출력
/error/5xx      - 500번대 에러일 때 출력
/error/4xx      - 400번대 에러일 때 출력
...

에러 페이지 출력

현재 프로젝트에서 Thymeleaf를 사용하고 있기 때문에 templates 밑에 error 폴더를 만들고 html을 작성했다.

이후 404 에러를 발생시켜보면 다음과 같이 4xx.html을 브라우저가 렌더링한 것을 확인할 수 있다.

Exception 발생

ErrorHandler 만들기

스프링 부트에서 error가 발생하면 로직을 타는 ErrorHandler를 만들어보자.

public class ErrorHandler implements HandlerExceptionResolver {
    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        log.info("ErrorHandler 동작");

        return null;
    }
}

HandlerExceptionResolver를 구현하면 되는데 사실 스프링 부트에서는 사실 기본으로 제공하는 HandlerExceptionResolver들이 있다. 하지만 로그를 찍어서 흐름을 보기위해 만들었다.

그리고 해당 extendHandlerExceptionResolvers 메서드를 통해 핸들러를 등록했다.

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
        resolvers.add(new ErrorHandler());
    }
}

로그 확인

다음 로그는 throw new 사용자정의예외();를 하는 컨트롤러에 요청을 보냈을 때의 로그이다. 로그 중간에 Exception에 대한 Stack Trace가 있어서 생략했다.

예외가 발생했기 때문에 인터셉터의 postHandle은 없는 것을 확인할 수 있다.
ErrorHandler는 인터셉터의 preHandlerafterCompletion에서 동작했다.



해당 ErrorHandler에서 Exception을 적절히 처리하면 WAS에서 다시 재요청하는 것을 막을 수 있다.

하지만 스프링 부트에서는 기본으로 다음 HandlerExceptionResolver를 제공하고 있다.

  • ResponseStatusExceptionResolver
  • DefaultHandlerExceptionResolver
  • ExceptionHandlerExceptionResolver

ResponseStatusException, DefaultHandlerExceptionResolver는 중간에서 결국 적절한 sendError를 보낸다. 그래서 WAS에서 재요청해서 BasicErrorController에서 처리한다.

만약 재요청하지 않고 바로 보내고 싶다면 ExceptionHandlerExceptionResolver에 대해 찾아보자.

참고

인프런 - 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술

profile
공부한 내용을 적지 말고 이해한 내용을 설명하자

0개의 댓글