서블릿 예외 처리
개요
- 스프링의 예외처리를 설명하기 전, 순수한 서블릿이 예외를 어떻게 처리하는지 알아보자.
-
먼저 새 프로젝트를 생성하자.
서블릿이 처리하는 예외 종류
- 일반 예외 (Exception)
- response.sendError(HTTP상태코드, 오류메시지)
예외처리 설명을 위한 설정: application.properties
server.error.whitelabel.enabled=false
스프링 부트가 기본적으로 제공하는 예외 페이지(WhiteLabel)를 잠시 꺼둬야한다.
웹 애플리케이션의 예외 흐름
예외(Exception) 흐름
- 웹 애플리케이션은 사용자 요청별로 쓰레드가 할당되고, 서블릿 컨테이너 안에서 실행된다.
- 웹 애플리케이션이 예외를 잡지 못하고 서블릿 밖으로 예외가 전달되면, 결국 WAS(톰캣)까지 예외가 전달되고 오류 페이지를 보여준다.
response.sendError() 흐름
- 컨트롤러에서 예외가 발생하지 않고 오직
sendError()
를 호출했을 때, 인터셉터와 서블릿, 필터, WAS에 예외가 전파되지 않는다.- 왜냐하면, 컨트롤러에서 예외가 발생한 적이 없고 단순히
sendError()
를 호출했기 때문이다.
- 왜냐하면, 컨트롤러에서 예외가 발생한 적이 없고 단순히
- WAS까지 전달받은 예외가 없지만,
sendError()
가 호출된 기록을 WAS가 확인하여 오류 페이지를 보여준다.
예외 처리 예시
예외 발생 코드: ExceptionController
클래스
import ...
@Controller
public class ExceptionController {
@GetMapping("/exception")
public void throwException() {
throw new RuntimeException("사용자 예외 발생!");
}
}
호출 결과
- 호출:
http://localhost:8080/exception
- 컨트롤러에서 발생한 예외가 처리되지 못하고 WAS까지 전파되어, 클라이언트에게 에러페이지를 보여준다.
sendError() 처리 예시
sendError() 코드: ExceptionController
클래스
import ...
@Controller
public class ExceptionController {
@GetMapping("/exception")
public String throwException() {
throw new RuntimeException("사용자 예외 발생!");
}
@GetMapping("/sendError")
public void sendErrorToWAS(HttpServletResponse response) throws IOException {
response.sendError(500, "sendError 호출!");
}
}
호출 결과
- 호출:
http://localhost:8080/sendError
- 컨트롤러에서 호출한
sendError()
가 처리되지 못하고 WAS가 호출기록을 확인하여, 클라이언트에게 에러페이지를 보여준다.
오류 화면 제공
- 서블릿 컨테이너가 제공하는 기본 예외 처리 화면은 고객 친화적이지 않다.
- 순수 서블릿:
web.xml
파일을 통해 오류 페이지 등록 - 스프링부트를 통한 서블릿:
implements WebServerFactoryCustomizer<ConfigurableWebServerFactory>
오류 화면 등록: WebServerCustomizer
클래스
import ...
@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);
}
}
ErrorPage(설정할_오류_종류, 내부요청할_URL)
재요청 처리 컨트롤러: ErrorPageController
클래스
import ...
@Controller
public class ErrorPageController {
@RequestMapping("/error-page/404")
public String errorPage404(HttpServletRequest request, HttpServletResponse response) {
return "error-page/404";
}
@RequestMapping("/error-page/500")
public String errorPage500(HttpServletRequest request, HttpServletResponse response) {
return "error-page/500";
}
}
재요청에 대해선 뒤에 자세히 설명한다. 일단 코드에 집중하자.
오류처리 뷰 템플릿: 404.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>404 오류 페이지!</h1>
</body>
</html>
오류처리 뷰 템플릿: 500.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>500 오류 페이지!</h1>
</body>
</html>
결과 1
-
요청URL
http://localhost:8080/존재하지않는페이지
-
결과
결과 2
-
요청URL
http://localhost:8080/sendError
-
결과
오류 페이지 작동 원리
Exception
(예외)가 발생하여 서블릿 밖으로 전달되는 경우response.sendError()
가 호출되는 경우
서블릿은 위 두가지 경우에 설정된 오류 페이지를 찾는다.
예외 발생 흐름과 내부요청
- WAS는 해당 예외를 처리하는 오류 페이지 정보를 확인한다.
new ErrorPage(오류종류, 내부요청할_URL)
new ErrorPage(RuntimeException.class, "/error-page/500")
등
- 해당 오류 페이지 정보 중 “내부요청할 URL 정보”를 참고하여 재요청한다.
sendError()
흐름과 내부요청
- WAS는 해당 예외를 처리하는 오류 페이지 정보를 확인한다.
new ErrorPage(오류종류, 내부요청할_URL)
new ErrorPage(RuntimeException.class, "/error-page/500")
등
- 해당 오류 페이지 정보 중 “내부요청할 URL 정보”를 참고하여 재요청한다.
Point!
- 웹 브라우저(클라이언트)는 서버 내부에서 이런 일이 일어났는지 전혀 모른다.
- 오직 서버 내부에서 오류 페이지를 찾기 위해 추가적인 호출을 한다.
예외 발생시, 서버가 담아주는 예외 정보
- 예외가 발생하면, WAS가 내부적으로 재요청을 수행한다.
- 이때,
HttpServletRequest
객체에 오류 정보를 담아준다.
오류 정보 종류
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 상태 코드
오류 정보 사용 예시: ErrorPageController
클래스
import ...
@Controller
public class ErrorPageController {
@RequestMapping("/error-page/500")
public String errorPage500(HttpServletRequest request, HttpServletResponse response) {
request.getAttribute("javax.servlet.error.status_code");
}
}
필터를 활용한 예외 처리
- 서버 내부에서 오류 페이지를 호출할 때마다, 필터와 인터셉터가 다시 호출된다. 이것은 매우 비효율적이다.
DispatcherType
이라는 추가 정보를 통해, 이러한 문제를 해결할 수 있다.DispatcherType
은 어떤 종류의 요청인지 알려준다.
DispatcherType
의 종류
REQUEST
- 클라이언트 요청
ERROR
- 오류 요청
FORWARD
- 서블릿에서 다른 서블릿이나 JSP를 호출할 때
INCLUDE
- 서블릿에서 다른 서블릿이나 JSP의 결과를 포함할 때
ASYNC
- 서블릿 비동기 호출
필터와 DispatcherType
활용
-
Log를 찍는 필터에
DispatcherType
을 적용하여, 내부호출에는 필터가 적용되지 않도록 구현한다. (목표: 오류시에는 로그를 찍지 않도록 구현)원래 오류시에 로깅을 통해 기록을 남기는 것이 좋지만, 예시를 위해 이와 같이 진행한다.
-
필터 구현:
LogFilter
클래스import ... @Slf4j public class LogFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { try { log.info("로그 필터 doFilter 호출"); chain.doFilter(request, response); } catch (Exception e) { throw e; } } @Override public void init(FilterConfig filterConfig) throws ServletException { log.info("로그 필터 init 호출"); } @Override public void destroy() { log.info("로그 필터 destroy 호출"); } }
- 필터 등록:
WebConfig
클래스DispatcherType
을 활용하여, 필터를 적용하지 않을 요청 구분
import ... @Configuration public class WebConfig { @Bean public FilterRegistrationBean logFilter() { FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>(); filterFilterRegistrationBean.setFilter(new LogFilter()); filterFilterRegistrationBean.setOrder(1); filterFilterRegistrationBean.addUrlPatterns("/*"); //DispatcherType 이 REQUEST인 경우에만 필터 호출 filterFilterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST); /* filterFilterRegistrationBean.setDispatcherTypes(DispatcherType.ERROR); 위 코드작성시, ERROR 일때만 (내부호출일때) 필터가 적용된다. */ return filterFilterRegistrationBean; } }
setDispatcherTypes(DispatcherType.REQUEST)
DispatcherType
이 REQUEST인 요청에 대해서만 필터가 동작한다.
전체 흐름 정리
/hello
정상 요청
/exception
오류 요청
- 필터는
DispatchType
으로 중복 호출 제거
인터셉터의 경우,
excludePathPatterns()
메서드를 통해 호출이 안되도록 설정할 수도 있다.
- 본 게시글은 김영한님의 강의를 토대로 정리한 글입니다.
- 더 자세한 내용을 알고 싶으신 분들이 계신다면, 해당 강의를 수강하시는 것을 추천드립니다.