1. 서블릿 예외 발생 처리 방식
서블릿은 2가지 방식으로 예외 처리를 지원한다.
- Exception
- response.sendError(HTTP 상태코드, 오류 메시지)
Exception이 발생하는 경우는 다시 2가지로 나뉜다.
자바 실행
통상 main 쓰레드를 실행하는 경우이며, 미리 짜놓은 코드에 의해 실행 중 예외가 발생하지 않고 main 메서드를 넘어서 예외가 발생하면 예외 정보를 남기고 쓰레드가 종료된다.
웹 애플리케이션
웹 애플리케이션은 요청별로 쓰레드가 할당되고, 서블릿 컨테이너 안에서 실행된다. 자바 실행의 경우와 마찬가지로 미리 짜놓은 try ~ catch 문 등으로 예외를 잡아내면 상관없지만, 예외를 잡지 못해서 서블릿 밖으로까지 예외가 전달되면 아래와 같은 도식으로 예외가 전파된다.
WAS <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)
2. 서블릿 예외 확인해보기
프로젝트 생성
프로젝트를 생성한다.
스프링 부트 예외 페이지 off
서블릿의 예외를 정확히 보기 위해서, 스프링 부트가 제공하는 기본 예외 페이지(자주 보던 whiteLabel 페이지)를 미사용으로 설정하자.
application.properties에 추가
server.error.whitelabel.enabled=false
서블릿 오류 페이지 살펴보기
그리고 ServletExceptionController를 하나 작성한다.
java/hello/exception/servlet/ServletExceptionController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@Slf4j
@Controller
public class ServletExceptionController {
@GetMapping("/error-ex")
public void errorEx() {
throw new RuntimeException("예외 발생!");
}
@GetMapping("/error-404")
public void error404(HttpServletResponse response) throws IOException {
response.sendError(404, "404 오류!");
}
@GetMapping("/error-500")
public void error500(HttpServletResponse response) throws IOException {
response.sendError(500, "500 오류!");
}
}
|
cs |
Exception 발생 시
기본 path("/")로 접속 시 아래 화면과 같은 404 에러 페이지가 표시된다. 서버에서 지정해준 경로에 해당하지 않기 때문에, Not Found를 의미하는 404 에러가 표시되는 것이다.
그리고 Exception을 발생시키기 위해 /error-ex 페이지로 접속하면 500 에러가 발생했다는 표시가 뜬다.
response.sendError 발생 시
/error-400, /error-500 으로 접속 시 위 Exception과 404, 500 으로 동일한 페이지가 출력된다. 그러나 내부적으로는 Controller에서부터 각 단계별 주체의 계속된 return을 통해 WAS 까지 전달되는 중간에, 서블릿 컨테이너가 sendError()가 호출되었는지 확인하고 호출된 경우 미리 설정된 기본 오류 페이지를 보여주는 원리로 에러 페이지가 출력된다.
3. 오류 화면 직접 만들기
앞서 살펴본 기본 제공 화면만으로는 상세한 정보를 클라이언트에게 전달하지 못할 뿐만 아니라 신뢰감을 주기 어렵다. 따라서 서블릿이 제공하는 오류 화면 설정 기능을 통해 오류 화면을 커스터마이징 해본다.
컨트롤러
컨트롤러는 기본 서블릿 오류 페이지때와 거의 유사하게 작성한다. 다만 view를 return하여, 404와 500의 경우에 각각 "error-page/404", "error-page/500"의 view로 이동하도록 만든다.
java/hello/exception/servlet/ErrorPageController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@Slf4j
@Controller
public class ErrorPageController {
@RequestMapping("/error-page/404")
public String errorPage404(HttpServletRequest request, HttpServletResponse response) {
log.info("errorPage 404");
return "error-page/404";
}
@RequestMapping("/error-page/500")
public String errorPage500(HttpServletRequest request, HttpServletResponse response) {
log.info("errorPage 500");
return "error-page/500";
}
}
|
cs |
WebServerCustomizer 작성 : WebServerFactoryCustomizer<ConfigurableWebServerFactory>
서블릿이 제공하는 WebServerCustomizer를 작성한다. 따로 문법을 외울 필요는 없다. 단지 new ErrorPage 객체를 통해 오류 타입과 오류 발생 시 이동하는 View 주소를 url로 넘겨주고, factory에 addErrorPages 형태로 에러 페이지들을 담으면 된다. 사용을 위해 @Component로 빈으로 등록해주어야 한다.
java/hello/exception/WebServerCustomizer.java
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@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);
}
}
|
cs |
View 전달 및 결과 확인
- 404, 500. html 에러 페이지를 만든다. 강의에서 제공하는 코드로 복사해오면 된다. 오류 화면이 커스터마이징 되는 것을 확인할 수 있다.
4. 오류 페이지 작동 원리
기본 작동 원리
작동 원리를 이해하는 것이 중요하다. Exception 이나 sendError의 전파는 위에서 살펴본 것과 같은 흐름으로 진행된다.
[1] WAS <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(오류 발생)
그런데, 오류 페이지를 요청하는 단계는 이와 정반대로 이루어진다.
[2] WAS에서 /error-page/500으로 다시 서버 요청 -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러 -> View
이렇게 되면, 서버에서 내부적으로 요청이 다시 전달되면서 필터와 인터셉터를 호출하고 불필요한 필터 및 인터셉터의 코드를 수행하게 된다. 예를 들어서 [1]과 같이첫 요청에서 회원의 로그인 검사를 해서 인터셉터를 통과하고, 로직이 진행되다가 Controller에서 에러가 발생했는데, [2]와 같이 WAS에서 에러 페이지를 다시 요청하는 단계에서 다시 로그인 검사를 인터셉터에서 할 수 있다. 따라서 이것을 막을 수 있는 다른 요소가 필요하다. 이 내용은 뒷부분에서 다시 배우게 된다.
오류 발생 내용 전달
WAS는 오류가 났을 때 단순히 오류 요청을 보내는 것이 아니라, 어떤 종류의 에러가 발생했는지를 request 객체에 담아서 보내준다. ErrorPageController에 request에 담긴 정보들을 출력해보자.
java/hello/exception/servlet/ServletExceptionController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
@Slf4j
@Controller
public class ErrorPageController {
public static final String ERROR_EXCEPTION = "javax.servlet.error.exception";
public static final String ERROR_EXCEPTION_TYPE = "javax.servlet.error.exception_type";
public static final String ERROR_MESSAGE = "javax.servlet.error.message";
public static final String ERROR_REQUEST_URI = "javax.servlet.error.request_uri";
public static final String ERROR_SERVLET_NAME = "javax.servlet.error.servlet_name";
public static final String ERROR_STATUS_CODE = "javax.servlet.error.status_code";
@RequestMapping("/error-page/404")
public String errorPage404(HttpServletRequest request, HttpServletResponse response) {
log.info("errorPage 404");
printErrorInfo(request);
return "error-page/404";
}
@RequestMapping("/error-page/500")
public String errorPage500(HttpServletRequest request, HttpServletResponse response) {
log.info("errorPage 500");
printErrorInfo(request);
return "error-page/500";
}
private void printErrorInfo(HttpServletRequest request) {
log.info("ERROR_EXCEPTION : {}", request.getAttribute(ERROR_EXCEPTION));
log.info("ERROR_EXCEPTION_TYPE : {}", request.getAttribute(ERROR_EXCEPTION_TYPE));
log.info("ERROR_MESSAGE : {}", request.getAttribute(ERROR_MESSAGE));
log.info("ERROR_REQUEST_URI : {}", request.getAttribute(ERROR_REQUEST_URI));
log.info("ERROR_SERVLET_NAME : {}", request.getAttribute(ERROR_SERVLET_NAME));
log.info("ERROR_STATUS_CODE : {}", request.getAttribute(ERROR_STATUS_CODE));
log.info("dispatchType={}", request.getDispatcherType());
}
}
|
cs |
출력 결과, 에러에 대한 상세 내용들을 담고 있는 것을 확인할 수 있다. dispatcherType 부분은 다음 섹션에서 다룬다.
5. DispatcherType
DispatcherType 이란
4. 오류 페이지 작동 원리 섹션에서 WAS에 의한 오류 페이지 호출 시, 필터, 인터셉터 등이 중복 호출되는 문제가 있다고 했다. 이를 해결하기 위한 표시 정보가 DispatcherType이라는 정보이다. 서블릿은 이 DispatcherType 정보를 기반으로 실제 클라이언트가 요청한 정보인지, 서버가 내부에서 오류 페이지를 요청하는 것인지를 구분한다.
DispatcherType의 종류
DispatcherType의 종류는 다음과 같다. 여기서 살펴볼 것은 REQUEST와 ERROR 부분이다. 첫 클라이언트의 요청에서는 DispatcherType이 REQUEST로 지정된다. 그러나, WAS에서 다시 Controller로 에러페이지를 요청할 때는 ERROR로 요청되어, 필터나 인터셉터를 거치지 않을 수 있게 된다.
REQUEST : 클라이언트 요청
ERROR : 서버 내부 오류 요청
FORWARD : 다른 서블릿이나 JSP 호출 (RequestDispatcher.forward)
INCLUDE : 서블릿에서 다른 서블릿이나 JSP 결과 포함
ASYNC : 서블릿 비동기 호출
DispatcherType 호출 확인
실제 DispatcherType이 어떻게 호출되는지 확인한다. 이전 글에서 사용했던 LogFilter 파일을 거의 그대로 복사해올건데, 로그에 request.getDispatcherType()만 추가한다.
java/hello/exception/filter/LogFilter.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
@Slf4j
public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("log filter init");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestURI = httpRequest.getRequestURI();
String uuid = UUID.randomUUID().toString();
try {
log.info("REQUEST [{}][{}][{}]", uuid, request.getDispatcherType(), requestURI);
chain.doFilter(request, response);
} catch (Exception e) {
log.info("EXCEPTION {}", e.getMessage());
throw e;
} finally {
log.info("RESPONSE [{}][{}][{}]", uuid, request.getDispatcherType(), requestURI);
}
}
@Override
public void destroy() {
log.info("log filter destroy");
}
}
|
cs |
그리고 WebConfig로 FilterRegistrationBean을 등록한다. .setDispatcherTypes() 메서드로 REQUEST, ERROR 타입을 등록했다. 이렇게 함으로써 두 타입의 요청이 왔을 때 filter가 작동하도록 하는 것이다. 그럼 설정한대로 LogFilter의 내용들이 출력될 것이다. 만약 이 값들을 아무것도 넣지 않으면 default로 REQUEST인 경우만 필터가 적용된다.
java/hello/exception/WebConfig.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@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;
}
}
|
cs |
실행 확인
/error-ex 페이지로 접속해보자. 그러면 아래와 같은 순서로 로그 및 예외가 발생되는 것을 확인할 수 있다. 네 문단이 보이는데, 차례대로 LogFilter의 try-catch-finally 부분과 함께 살펴보자.
우선 클라이언트에서 요청이 왔으니, REQEUST로 요청이 간다. 그렇게 try 구문이 실행되고, chain.doFilter를 통해서 다음 필터를 요청하나, 필터가 없으므로 서블릿이 호출된다. 그리고 catch 구문에서 EXCEPTION을 잡고, fianlly 구문에서 RESPONSE를 출력한다.
dispatcherServlet에 의해 두번째 문단인 예외 발생!이 출력된다.
세번째 문단에서, REQUEST가 ERROR DispatcherType으로 호출된 것을 확인할 수 있다. 이후 LogFilter에서 바로 ErrorPageController로 넘어간 것이 보인다. 이후 네번째 구문 이후로는 에러에 대한 정보가 호출되다가, 마지막으로 finally 구문에 의해 RESPONSE가 출력된다.
LogFilter의 try-catch-finally 부분
참조
1. 인프런_스프링 MVC 2편 - 백엔드 웹개발 핵심 기술_김영한 님 강의
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2
'Programming-[Backend] > Spring' 카테고리의 다른 글
[스프링 웹MVC-2] 16. API 예외 처리 - 스프링부트 기본 오류 처리, HandlerExceptionResolver (0) | 2021.09.09 |
---|---|
[스프링 웹MVC-2] 15. 예외 - 인터셉터와 스프링 부트의 오류 페이지 (0) | 2021.09.08 |
[스프링 웹MVC-2] 13. 로그인 - 스프링 인터셉터, ArgumentResolver 활용 (0) | 2021.09.05 |
[스프링 웹MVC-2] 12. 로그인 - 서블릿 필터 (0) | 2021.09.01 |
[스프링 웹MVC-2] 11. 로그인 - 세션 활용 (0) | 2021.08.29 |