웹 애플리케이션 동작 중 예외가 발생하면 예외를 처리하고 필요하다면 사용자가 알 수 있도록 오류 페이지를 응답하는 것이 좋은 웹 애플리케이션이다. 이를 위해 스프링 MVC의 예외 처리와 오류 페이지 응답 구조를 알아보자.
여기서 오류 페이지란 클라이언트에게 오류를 알리는 뷰(ex - html 문서)이다. 만약 뷰가 아닌 API로 정의된 데이터(ex - json)만을 응답해야하는 API 서버에서는 각 오류(예외) 상황에 맞는 오류 응답 스펙을 정하고 스펙에 맞는 데이터를 응답하면 된다.
예외 처리와 오류 페이지 응답
스프링 MVC는 톰캣과 같은 서블릿 컨테이너를 기반으로 동작하고 서블릿 컨테이너는 자바를 기반으로 동작한다. 따라서 스프링 MVC의 예외 처리 구조를 파악하려면 자바 예외 처리 - 서블릿 컨테이너 예외 처리 - 스프링 MVC 예외처리 순의 이해가 필요하다.
자바 예외 처리
예외를 상위 메서드로 던질 수 있다. 따라서 예외는 점차 상위 메서드로 전파된다. 만약 스레드의 메인 메서드에서 예외를 잡지 못하면, 예외 정보를 남기고 해당 스레드는 종료된다.
서블릿 컨테이너(WAS) 예외 처리
서블릿 컨테이너는 요청 별로 스레드를 할당하고 할당된 스레드가 서블릿을 수행해 응답한다. 이때 웹 어플리케이션에 예외가 발생했는데, 애플리케이션에서 예외를 잡지 못하고 서블릿 컨테이너까지 예외가 전달되면 어떻게 될까?
WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)
서블릿 컨테이너까지 예외가 전파되거나, HttpServletResponse가 제공하는 sendError 메서드를 사용하면 서블릿 컨테이너는 해당 예외, 오류에 해당하는 오류 페이지 경로를 찾고 해당 경로로 다시 요청한다. 즉 아래와 같이 동작한다. 리다이렉트가 아니라는 점에 유의하자. 참고로 서블릿 컨테이너는 재요청시 응답 코드를 예외가 전파된경우 500, 서버에 없는 리소스인경우 404, response.sendError()를 호출한경우 인자로 지정한 코드로 설정한다.
1. WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)
2. WAS `/error-page/500` 다시 요청 -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러(/errorpage/500) -> View
sendError 메서드
response.sendError() 를 호출하면 response 내부에 오류가 발생했다는 상태를 저장해 둔다. 그리고 서블릿 컨테이너는 고객에게 응답 전에 response에 sendError()가 호출되었는지 확인하고 만약 호출되었다면 설정한 오류 코드에 해당하는 오류 페이지 경로를 다시 요청한다.
오류 페이지 경로 지정
서블릿 컨테이너가 기본으로 제공하는 오류 페이지는 매우 단순하다. 오류 페이지를 직접 제공하기 위해 서블릿 컨테이너가 요청하는 오류 페이지 경로를 지정할 수 있다. 과거에는 web.xml 파일에 오류 페이지 경로를 지정했지만 스프링 부트를 사용한다면 스프링 부트의 기능으로 오류 페이지 경로를 지정할 수 있다.
package hello.exception;
import org.springframework.boot.web.server.ConfigurableWebServerFactory;
import org.springframework.boot.web.server.ErrorPage;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
@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 errorPageEx = new ErrorPage(RuntimeException.class, "/error-page/500");
factory.addErrorPages(errorPage404, errorPage500, errorPageEx);
}
}
- response.sendError(404) 호출 → /error-page/404 요청
- response.sendError(500) 호출 → /error-page/500 요청
- RuntimeException 또는 자식 타입의 예외 → /error-page/500 요청
이후 오류 페이지 경로를 처리하는 컨트롤러를 만들고 컨트롤러에서 오류 페이지를 응답하면 된다.
오류 정보 추가
서블릿 컨테이너는 단순히 오류 페이지 경로로 다시 요청만 하는 것이 아니라 오류 정보를 HttpServletRequest 객체의 attribute에 추가해서 넘겨준다. 아래의 정보들이 추가된다.
- javax.servlet.error.exception : 예외
- javax.servlet.error.exception_type : 예외 타입
- javax.servlet.error.message : 오류 메시지
- javax.servlet.error.request_uri : 클라이언트 요청 URI
- javax.servlet.error.servlet_name : 오류가 발생한 서블릿 이름
- javax.servlet.error.status_code : HTTP 상태 코드
필터 중복 호출 방지
오류가 발생하면 서블릿 컨테이너가 오류 페이지 경로로 다시 요청을 한다. 이 과정에서 필터, 서블릿, 인터셉터도 모두 호출되지만 필터와 인터셉터가 다시 호출되는 것은 비효율적이다. 서블릿은 필터 중복 호출 문제를 해결하기 위해 HttpServletRequest 객체에 dispatcherType이라는 추가 정보를 제공한다.
처음 요청 시 dispatcherType의 값은 REQUEST이지만 오류가 발생해 서블릿 컨테이너가 오류 페이지 경로로 다시 요청하면 dispatcherType의 값은 ERROR가 된다. 아래와 같이 필터의 setDispatcherTypes 메서드로 dispatcherType 값에 따른 필터 호출 여부를 설정할 수 있다. 기본 값은 REQUEST이다. 아래의 필터는 요청의 dispatcherType값이 REQUEST 또는 ERROR 일 때만 호출된다.
package hello.exception;
import hello.exception.filter.LogFilter;
import hello.exception.interceptor.LogInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
@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);
return filterRegistrationBean;
}
}
인터셉터 중복 호출 방지
인터셉터는 excludePathPatterns으로 오류 페이지 경로를 넣어 중복 호출을 방지하면 된다.
package hello.exception;
import hello.exception.interceptor.LogInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/**", "*.ico", "/templates/error1", "/error-page/**"); //오류 페이지 경로 추가
}
}
스프링 부트 예외 처리
지금까지 오류 페이지를 만들기 위해 WebServerCustomizer를 만들고 예외 종류에 따라서 ErrorPage로 오류 페이지 경로를 추가하고 오류 페이지 경로를 처리하는 컨트롤러를 만들었지만 스프링 부트는 이런 과정을 기본으로 제공한다.
- 스프링 부트는 ErrorPage를 자동으로 등록한다. 이때 /error라는 경로를 지정한다. 때문에 스프링 부트 사용 시 서블릿 밖으로 예외가 발생하거나, response.sendError()가 호출되면 서블릿 컨테이너는 /error 경로로 다시 요청한다. (참고로 ErrorMvcAutoConfiguration 클래스가 ErrorPage를 자동으로 등록한다.)
- 스프링 부트는 /error를 매핑해서 처리하는 컨트롤러인 BasicErrorController를 자동으로 등록한다. BasicErrorController 로직으로 오류 페이지가 응답된다.
개발자는 오류 페이지만 BasicErrorController가 제공하는 룰과 우선순위에 따라서 등록하면 된다.
BasicErrorController의 뷰 선택 우선순위
- 뷰 템플릿
- resources/templates/error/500.html
- resources/templates/error/5xx.html
- 정적 리소스(static , public)
- resources/static/error/400.html
- resources/static/error/404.html
- resources/static/error/4xx.html
- 적용 대상이 없을 때 뷰 이름(error)
- resources/templates/error.html
해당 경로 위치에 HTTP 상태 코드 이름의 뷰 파일을 넣어두면 된다. 뷰 템플릿이 정적 리소스보다 우선순위가 높고, 404, 500처럼 구체적인 것이 5xx처럼 덜 구체적인 것보다 우선순위가 높다. 5xx, 4xx라고 하면 500대, 400대 오류를 처리해 준다.
스프링 부트 오류 관련 옵션
- server.error.whitelabel.enabled=true : 오류 처리 화면을 못 찾을 시, 스프링 whitelabel 오류 페이지 적용
- server.error.path=/error : 오류 페이지 경로, 스프링이 자동 등록하는 서블릿 글로벌 오류 페이지 경로와 BasicErrorController 오류 컨트롤러 경로에 함께 사용된다.
'Spring > Spring MVC' 카테고리의 다른 글
API 예외 처리 (0) | 2023.04.19 |
---|---|
ArgumentResolver 활용 (0) | 2023.04.17 |
스프링 Filter, Interceptor (0) | 2023.04.14 |
스프링 검증 (2) (0) | 2023.04.02 |
스프링 검증 (1) (0) | 2023.03.27 |