![[Spring] 쉬우면서 정확하게 익혀보는 필터와 인터셉터의 예외처리 흐름과 예외 페이지 응답](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbj4gyE%2FbtsJq7NQgDq%2FcVKtQs6oWGv2eM8qel2l9k%2Fimg.png)
스프링을 사용하지 않는 순수 서블릿 컨테이너는 Exception
과 response.sendError(Http상태코드, 오류메시지)
두 가지 방식으로 예외를 처리한다.
Exception으로 처리하기
기본적으로 자바는 예외가 발생하면 예외를 다음 메서드로 계속 넘기다가 main()
메서드가 종료될 때 까지 예외를 처리하지 못할 경우 예외 정보를 남기고 해당 쓰레드는 종료된다.
웹 애플리케이션은 쓰레드가 하나만 있는것이 아니라 사용자별로 각각의 쓰레드가 할당된다.
애플리케이션 어딘가에서 예외가 발생하고 그 예외를 처리하지 않고 서블릿 밖으로 까지 예외가 전달된다면 어떻게 될까?
컨트롤러(예외발생) -> 인터셉터 -> 서블릿 -> 필터 -> WAS
예외는 타고타고 올라가서 WAS까지 전달되며 WAS는 서버에서 발생한 예외이기때문에 500 Internal Server Error를 발생하고 서블릿에 기본값으로 지정된 해당 에러 페이지를 보여준다.
response.sendError
를 사용하면 인자를 통해서 임의로 상태코드와 오류메시지를 지정하여 페이지를 보여줄 수 있다.
이 때 sendError
에 담긴 상태코드와 오류메시지 역시 WAS에서 확인한다.
오류 화면 등록
public class WebServerCustomizer implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
@Override
public void customize(ConfigurableWebServerFactory factory) {
ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-page/404");
ErrorPage errorPage500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error-page/500");
ErrorPage errorPageRuntime = new ErrorPage(RuntimeException.class, "/error-page/RuntimeException");
factory.addErrorPages(errorPage404, errorPage500, errorPageRuntime);
}
}
위 코드와 같이 WebServerFactoryCustomizer
를 구현하면 customize
메서드를 오버라이드해서 ErrorPage
를 정의해 놓을 수 있다.ErrorPage(예외 또는 HTTP상태코드, 처리할 경로)
ErrorPage
가 어떻게 브라우저를 통해 보여지는지 알아보자.
만약 컨트롤러에서 RuntimeException
이 발생되면 이 예외는 컨트롤러, 인터셉터, 필터...를 거쳐서 최종적으로는 WAS에 예외가 전달된다.
WAS는 전달받은 예외에 맞는 ErrorPage
가 등록되어있는지 확인한다.errorPageRuntime
이라는 객체로 정의되어있기때문에 /error-page/RuntimeException
을 호출한다.
이 과정에서 WAS->필터->인터셉터->컨트롤러를 다시 거쳐 View로 에러 페이지가 브라우저에 보여지는 것이다.
마치 HTTP 요청을 다시 보낸 것 같지만 실제로는 그렇지 않다.
참고로 오류페이지를 다시 불러오는 과정에서 request.attribute
에 오류에 대한 정보를 추가해서 넘겨준다.
필터, 인터셉터가 두 번 호출되는 문제
위 내용을 이해했다면 WAS가 에러를 인지하고 error-page
를 호출하는 과정에서 필터와 인터셉터가 다시 호출되는 것을 알 수 있다.
이미 처음 HTTP요청을 하는과정에서 필터와 인터셉터가 처리해야할 로직을 수행했지만 중복해서 수행하게 되어 불필요한 과정을 수행하게 되는 것이다.
서블릿에서는 이러한 문제를 해결하기위해서 DispatcherType이라는 정보를 제공한다.
DispatcherType은 Enum으로 정의되어있으며
- REQUEST
- ERROR
- FORWARD
- INCLUDE
- ASUNC
중 하나의 값을 가질 수 있다.
애플리케이션 로직에서 예외가 발생하고 WAS에서 해당 예외를 확인한 뒤, error-page
를 호출하기위해 임의의 요청을 다시 애플리케이션으로 보낼 때, DispatcherType의 값을 ERROR
로 설정해서 보낸다.
이렇게하면 애플리케이션은 해당 요청(?)이 일반적인 HTTP 요청인지 아니면 error-page
를 출력하기위한 요청인지 구분할 수 있게되는것이다. 참고로 일반 HTTP요청은 REQUEST
값이 설정되서 나간다.
이 값을 통해서 서블릿 필터의 불필요한 동작을 예방할 수 있을 것이다.
@Slf4j
public class LogFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain){
HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
String requestURI = httpRequest.getRequestURI();
String uuid = UUID.randomUUID()
.toString();
log.info("REQUEST [{}][{}][{}]", uuid, servletRequest.getDispatcherType(), requestURI);
filterChain.doFilter(httpRequest, servletResponse);
}
@Override
public void destroy() {
log.info("log filter destroy");
}
}
먼저 DispathcherType
필드가 어떻게 동작하는지 확인하기위해 LogFilter
라는 로그를 찍어주는 역할을 하는 서블릿 필터 클래스를 하나 생성하자.
첫번째 출력되는 로그는 식별자, DispatcherType
의 값, 요청 URI를 출력할 것이다.
이 때 예상되는 실행결과는
- UUID = 임의의 UUID
- DispatcherType = REQUEST
- requestURI = /error-ex
일 것이다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
// 이 필터(LogFilter)는 REQUEST와 ERROR요청시에 사용된다.
return filterRegistrationBean;
}
}
그리고 생성한 서블릿 필터를 적용하기위해 설정클래스인 WebConfig
를 생성해주고 해당 필터를 적용해보자.
setFilter
: 적용할 필터를 설정해준다setOrder
: 적용한 필터의 동작 순서를 정한다addUrlPatterns
: 필터가 수행하기위한 Request URI를 설정해준다setDispatcherType
: 필터를 수행하기위한DispatcherType
을 설정해준다
위 코드를 기반으로 동작을 유추해보자면,LogFilter
는 클라이언트로부터 요청이 들어올때 첫번째로 동작하며 이 때 동작하기위한 URI조건은 /*
즉, 모든 요청에 LogFilter
가 동작되도록한다.
또한 해당 요청의 DispatcherType
은 REQUEST
, ERROR
일 경우에만 동작하게 된다.
자 그럼 LogFilter
와 WebConfig
를 적용해준 상태로 애플리케이션을 실행하고 /error-ex요청을 하면 어떻게 될까? (요청시 컨트롤러에서 RuntimeException발생)
먼저 DispatcherType=REQUEST
인 상태에서 HTTP 정상요청이 들어오고
컨트롤러에서 예외가 발생하고 이 예외에 대한 처리를 하지 않았기때문에 WAS까지 예외가 올라간다
그리고 WAS는 등록된 error-page
목록을 확인한 뒤 유효한 예외라면, 다시 서블릿, 스프링 컨테이너로 돌려보낸다 (마치 HTTP 요청을 다시 보내는것같음)
그 과정에서 WAS는 DispatcherType=ERROR
로 설정하기때문에 서블릿 필터나 인터셉터에서 해당 상태를 구분할 수 있다!
실제 실행결과
[exception] .... : REQUEST [uuid][REQUEST][/error-ex]
[Request processing failed: java.lang.RuntimeException.....
java.lang.RuntimeException: 예외 발생
REQUEST [8a3d00a6-d7d5-4089-9071-fd9625b4e292][ERROR][/error-page/500]
errorPage 500
(불필요한 예외 메시지는 제거하거나 ...으로 생략했다)
[REQUEST], [ERROR] 각각 한 번씩 출력되는 것을 확인할 수 있다.
ㅇㅋㅇㅋ필터는 알겠어요, 그럼 인터셉터는요?
인터셉터도 서블릿 필터와 매우 유사하게 실제 HTTP요청인지, error-page
출력을 위한 요청(?)인지 구분할 수 있다.
차이점은 서블릿 필터는 DispatcherType=ERROR
라는 값을 통해 구분했지만,
스프링 인터셉터는 excludePathPatterns()
메서드의 인자에 인터셉터의 동작을 원치않는 요청경로를 설정해주면된다.
먼저 이전에 사용한 FilterRegistrationBean
의 @Bean
또는 해당 코드 자체를 주석처리해주자
@Slf4j
public class LogInterceptor implements HandlerInterceptor {
public static final String LOG_ID = "logId";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
String uuid = UUID.randomUUID()
.toString();
request.setAttribute(LOG_ID, uuid);
log.info("REQUEST [{}][{}][{}][{}]", uuid, request.getDispatcherType(), requestURI, handler);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle [{}]", modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
String requestURI = request.getRequestURI();
String logId =(String) request.getAttribute(LOG_ID);
log.info("RESPONSE [{}][{}][{}]", logId, request.getDispatcherType(), requestURI);
if (ex != null) {
log.error("afterCompletion error!!", ex);
}
}
}
간략하게 각각의 핸들링 메서드에대해 설명해보자면
preHandle
: 컨트롤러 메서드 호출전에 실행postHandle
: 컨트롤러 메서드 호출후 수행, 예외 발생시 수행되지 않음afterCompletion
: 뷰가 렌더링 된 후, 즉 전체 요청처리가 완료된 후 수행 - 예외가 발생해도 수행
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
//인터셉터를 제외할 요청주소들
//서블릿처럼 DispatcherType은 없지만, 에러페이지 자체를 요청리스트로부터 제외할 수 있다.
.excludePathPatterns("/css/**", "/*.ico", "/error", "/error-page/**");
}
}
인터셉터를 등록하기위한 설정 클래스를 수정하자
이전에 서블릿 필터를 등록하는 방법과 매우 유사하며 갓프링답게 set()
방식 보다는 메서드 체이닝 방식을 통해 보다 편리한 설정을 제공한다.
addInterceptor
: 커스텀한LogInterceptor
를 등록order
: 인터셉터의 동작순서 등록addPathPatterns
: 인터셉터를 수행하고자하는 요청경로설정 (필터의 addUrlPatterns)excludePathPatterns
: 인터셉터 수행할 필요 없는 요청경로를 설정
excludePathPatterns
메서드를 사용한다는 점이 서블릿 필터와 가장 큰 차이점인듯하다.
css, error, image와 같은 경로들은 인터셉터를 굳이 설정해줄 필요가 없다(해야할 경우도 있지만)
그런 경우에 해당 메서드안에 인자로 경로를 정의해주면 된다.
정리해보자
웹 애플리케이션에 error-page
를 등록해두고 사용할 때 애플리케이션 코드에서 예외가 발생하고, 해당 예외를 처리해주지 않으면 WAS까지 예외가 올라가고 해당 예외에 맞는 error-page
가 등록되어있다면 WAS는 재요청을 하는형태로 애플리케이션코드로 다시 보낸다.
그 과정에서 처음 REQUEST에 대한 필터, 인터셉터 동작과 error-page
를 호출하기위한 필터, 인터셉터 동작 각각 두번씩 동작한다는 것을 알 수 있었다.
이런 불필요한 중복동작을 막기위해 서블릿 필터는 DispatcherType
을, 인터셉터는 excludePathPatterns
를 이용하여 필터와 인터셉터의 중복동작을 막을 수 있었다.
이제 스프링 답게 부트를 통해 숟가락만 얹어보자
@Component
public class WebServerCustomizer implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
@Override
public void customize(ConfigurableWebServerFactory factory) {
ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-page/404");
ErrorPage errorPage500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error-page/500");
ErrorPage errorPageRuntime = new ErrorPage(RuntimeException.class, "/error-page/500");
factory.addErrorPages(errorPage404, errorPage500, errorPageRuntime);
}
}
처음에 error-page
를 편리하게 사용하기위해서 예외 또는 HTTP STATUS CODE에 맞는 오류 페이지를 응답하기위해 new ErrorPage
를 사용하여 오류 페이지를 미리 정의해두었었다.
그 과정에서 필터와 인터셉터의 동작을 알아보았었다.
ErrorPage
등록하고.. 이 에러페이지들을 WAS로부터 출력하기위해 에러 페이지들을 처리할 컨트롤러를 따로 등록해주고.. 프로젝트를 진행한다면 반복될만한 작업들을 계속 등록하고, 사용해주어야한다.
이럴때 꼭 스프링부트가 등장해서 번거로운 작업들을 대신해준다.
스프링 부트가 제공해주는 기능을 사용하기위해 WebServerCustomizer
의 @Component
어노테이션을 주석처리해주자. (참고로 스프링은 부트가 제공해주는 디폴트 설정보다 개발자가 직접정의한 설정파일과 Bean에 더 우선권을 부여한다.)
@Component
어노테이션을 주석처리했다면 이제 스프링 부트를 통해 더욱더 간편하고 심플하게 에러 페이지를 출력해보자.
스프링 부트는 위에서 우리가 직접구현했던 에러페이지와 이를 처리하는 컨트롤러를 구현해놓았으며 구현해놓은 자리에 우리가 사용하고자하는 에러 페이지를 만들고, 그 페이지를 등록하기만 하면된다.
어떻게 등록하느냐?
컨트롤러를 통해 처리하기때문에 먼저 스프링 부트가 설정한 경로를 알아야하는데 /error 라는 경로를 기본값으로 설정해놓았다.
스프링 부트는 BasicErrorController
라는 컨트롤러를 아래와 같이 정의해놓았다. (대충 이런느낌)
@RequestMapping("/error")
public class BasicErrorController{
@GetMapping("/404")
public String error404(){
return "/error/404";
}
}
이런식으로 컨트롤러가 구현되어있으며 우리는 에러페이지를 만들고, 만든 에러페이지의 경로를 옳바르게만 지정해주면된다.
만약 동적 처리할 데이터가 없다면 /resources/static/error
경로에
동적 처리를 위해 뷰템플릿을 등록한다면 /resources/templates/error
경로에 에러페이지 템플릿을 만들어주면 끝이다!
마지막으로 BasicErrorController에 대해 맛만 봐보자
BasicErrorController
는 실제 등록한 에러페이지를 매핑해줌과 동시에 model
에 여러가지 정보를 담아서 전달한다.model
에 담긴 정보는 thymeleaf와 같은 뷰 템플릿에서 데이터를 꺼내어 화면에 보여줄 수 있다.
- timestamp
- status
- error
- exception
- trace
- message
- errors
...등등 여러가지 정보를 담을 수 있다.
하지만 에러 페이지의 목적은 사용자가 웹 애플리케이션을 사용함에 있어서 어떤 요청을 보냈을 때, BadReqeust라던가 500 서버에러와 같은 잘못된 리소스 접근 또는 서버에 장애가 있다는 것을 표시할 뿐 그 외에 대한 정보를 넘겨주게 된다면 보안적으로 취약할 수 있고 사용자 입장으로는 알아듣기 어려운 불필요한 정보일 뿐이다.
따라서 굳이 BasicErrorController
가 넘겨준 model의 정보값을 사용할 일은 없을 것 같다.
하지만 굳이 사용하자면 개발과정에서 브라우저를 통해 직접 에러를 확인한다면 스택트레이스를 브라우저로 확인할 수 있도록 활용할 수는 있을 것 같다.
만약 이와 같이 BasicErrorController
가 넘겨준 model의 값을 사용하고싶다면 아래 설정옵션을 참고하자.
application.properties
server.error.include-exception=false || true
server.error.include-message=never || always
server.error.include-stacktrace=never || always
server.error.include-biding-errors=never || always
따로 설명하지않아도 어떤 옵션에 대한 설정인지 알 수 있을것이다.
기타 참고하면 좋을 수 있는 설정옵션server.error.whitelabel.enable=true || false
: 등록한 /error의 에러페이지가 존재하지 않는 오류가 발생할 경우 스프링 whitelabel을 응답한다
server.error.path=/error
: 에러 페이지를 등록할 경로를 지정한다.
참고로 BasicErrorController
를 상속받아 확장하여 기능을 추가할수도있다.
이 포스팅은 스프링 일타강사 인프런의 김영한님의 강의를 듣고 스스로 정리한 포스팅입니다!
https://www.inflearn.com/course/lecture?courseSlug=%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2&unitId=83355&tab=curriculum
'Framework > Spring' 카테고리의 다른 글
[Spring] - 스프링에서 예외처리를 위한 'HandlerExceptionResolver' 알아보기 (1) | 2024.09.07 |
---|---|
[Spring] - API예외 처리를 이해하기위한 기본 개념! (2) | 2024.09.06 |
[Spring] 서블릿 필터와 스프링 인터셉터 비교하기! (0) | 2024.08.07 |
[Spring] JdbcTemplate (0) | 2024.06.16 |
Spring - 메시지 기능으로 HTML하드코딩 제거하기 (0) | 2024.04.23 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!