- 해당 내용은, '인프런 - 스프링 MVC 2편 - 김영한님 강의'를 개인적으로 개념정리를 위해 정리한 내용입니다.
서블릿 예외 처리 - 시작
- 스프링이 아닌 순수 서블릿 컨테이는 예외를 어떻게 처리하는지 알아보자
- 서블릿은 다음 2가지 방식으로 예외를 처리한다.
- Exception (예외)
- response.sendError (HTTP 상태 코드, 오류 메시지)
- Exception(예외)
- 자바 직접 실행
- 자바의 메인 메서드를 직접 실행하는 경우 main 이라는 이름의 쓰레드가 실행된다. 실행 도중에 예외를 잡지 못하고 처음 실행한 main() 메서드를 넘어서 예외가 던져지면, 예외 정보를 남기고 해당 쓰레드는 종료된다.
- 웹 애플리케이션
- 웹 애플리케이션은 사용자 요청별로 별도의 쓰레드가 할당되고, 서블릿 컨테이너 안에서 실행된다. 애플리케이션에서 예외가 발생했는데, 어디선가 try ~ catch로 예외를 잡아서 처리하면 아무런 문제가 없다. 그런데 만약에 애플리케이션에서 예외를 잡지 못하고, 서블릿 밖으로 까지 예외가 전달되면 어떻게 동작할까?
- WAS(여기까지 전파) ← 필터 ← 서블릿 ← 인터셉터 ← 컨트롤러(예외발생) 결국 톰캣 같은 WAS 까지 예외가 전달된다.
- WAS는 예외가 올라오면 어떻게 처리해야 할까?
- 웹 애플리케이션은 사용자 요청별로 별도의 쓰레드가 할당되고, 서블릿 컨테이너 안에서 실행된다. 애플리케이션에서 예외가 발생했는데, 어디선가 try ~ catch로 예외를 잡아서 처리하면 아무런 문제가 없다. 그런데 만약에 애플리케이션에서 예외를 잡지 못하고, 서블릿 밖으로 까지 예외가 전달되면 어떻게 동작할까?
- 자바 직접 실행
- response.sendError(HTTP 상태 코드, 오류 메시지)
- 오류가 발생했을 때 HttpServletResponse 가 제공하는 sendError 라는 메서드를 사용해도 된다.
- 이것을 호출한다고 당장 예외가 발생하는 것은 아니지만, 서블릿 컨테이너에게 오류가 발생했다는 점을 전달할 수 있다. 이 메서드를 사용하면 HTTP 상태 코드와 오류 메시지도 추가할 수 있다. response.sendError(HTTP 상태 코드) response.sendError(HTTP 상태 코드, 오류 메시지)
- ServletExController - 추가
@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); }
- sendError 흐름
- WAS(sendError 호출 기록 확인) ← 필터 ← 서블릿 ← 인터셉터 ← 컨트롤러 (response.sendError())
- response.sendError() 를 호출하면 response 내부에는 오류가 발생했다는 상태를 저장해둔다. 그리고 서블릿 컨테이너는 고객에게 응답 전에 response 에 sendError() 가 호출되었는지 확인한다. 그리고 호출되었다면 설정한 오류 코드에 맞추어 기본 오류 페이지를 보여준다.
서블릿 예외 처리 - 오류 화면 제공
- 과거에는 web.xml 이라는 파일에 다음과 같이 오류 화면을 등록했다
<web-app>
<error-page>
<error-code>404</error-code>
<location>/error-page/404.html</location>
</error-page>
<error-page>
<error-code>500</error-code>
<location>/error-page/500.html</location>
</error-page>
<error-page>
<exception-type>java.lang.RuntimeException</exception-type>
<location>/error-page/500.html</location>
</error-page>
</web-app>
- 지금은 스프링 부트를 통해서 서블릿 컨테이너를 실행하므로, 스프링 부트가 제공하는 기능을 사용해서 서블릿 오류 페이지를 등록
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);
}
}
- 해당 오류를 처리할 컨트롤러가 필요
package hello.exception.servlet;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@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";
}
}
서블릿 예외 처리 - 오류 페이지 작동 원리
- 서블릿은 Exception (예외)가 발생해서 서블릿 밖으로 전달되거나 또는 response.sendError() 가 호출 되었을 때 설정된 오류 페이지를 찾는다
- 예외 발생 흐름
- WAS(여기까지 전파) ← 필터 ← 서블릿 ← 인터셉터 ← 컨트롤러(예외발생)
- sendError 흐름
- WAS(sendError 호출 기록 확인) ← 필터 ← 서블릿 ← 인터셉터 ← 컨트롤러 (response.sendError())
- 예를 들어서 RuntimeException 예외가 WAS까지 전달시, WAS는 오류 페이지 정보를 확인한다. 확인해보니 RuntimeException의 오류 페이지로 /error-page/500이 지정되어 있다. WAS는 오류 페이지를 출력하기 위해 /error-page/500를 다시 요청한다.
- 오류 페이지 요청 흐름
- WAS /error-page/500 다시 요청 → 필터 → 서블릿 → 인터셉터 → 컨트롤러(/error-page/ 500) → View
- 예외 발생과 오류 페이지 요청 흐름
-
- WAS(여기까지 전파) ← 필터 ← 서블릿 ← 인터셉터 ← 컨트롤러(예외발생)
- WAS /error-page/500 다시 요청 → 필터 → 서블릿 → 인터셉터 → 컨트롤러(/error- page/500) → View
-
- 오류 정보 추가
- WAS는 오류 페이지를 단순히 다시 요청만 하는 것이 아니라, 오류 정보를 request 의 attribute 에 추가해서 넘겨준다.
- 필요하면 오류 페이지에서 이렇게 전달된 오류 정보를 사용할 수 있다
- ErrorPageController - 오류 출력
package hello.exception.servlet;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Slf4j
@Controller
public class ErrorPageController {
//RequestDispatcher 상수로 정의되어 있음
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: ex=", request.getAttribute(ERROR_EXCEPTION));
log.info("ERROR_EXCEPTION_TYPE: {}", request.getAttribute(ERROR_EXCEPTION_TYPE));
log.info("ERROR_MESSAGE: {}", request.getAttribute(ERROR_MESSAGE)); // ex의 경우 NestedServletException 스프링이 한번 감싸서 반환
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());
}
}
- request.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 상태 코드
서블릿 예외 처리 - 필터
- 오류가 발생하면 오류 페이지를 출력하기 위해 WAS 내부에서 다시 한번 호출이 발생한다.
- 이때 필터, 서블릿, 인터셉터도 모두 다시 호출된다. 그런데 로그인 인증 체크 같은 경우를 생각해보면, 이미 한번 필터나, 인터셉터에서 로그인 체크를 완료했다.
- 따라서 서버 내부에서 오류 페이지를 호출한다고 해서 해당 필터나 인터셉트가 한번 더 호출되는 것은 매우 비효율적이다.
- 결국 클라이언트로 부터 발생한 정상 요청인지, 아니면 오류 페이지를 출력하기 위한 내부 요청인지 구분할 수 있어야 한다.
- 서블릿은 이런 문제를 해결하기 위해 DispatcherType 이라는 추가 정보를 제공한다
- DispatcherType
- 필터는 이런 경우를 위해서 dispatcherTypes 라는 옵션을 제공한다.
- 이전 강의의 마지막에 다음 로그를 추가했다.
- log.info("dispatchType={}", request.getDispatcherType())
- 그리고 출력해보면 오류 페이지에서 dispatchType=ERROR 로 나오는 것을 확인할 수 있다.
- 고객이 처음 요청하면 dispatcherType=REQUEST 이다.
- 이렇듯 서블릿 스펙은 실제 고객이 요청한 것인지, 서버가 내부에서 오류 페이지를 요청하는 것인지 DispatcherType 으로 구분할 수 있는 방법을 제공한다
- javax.servlet.DispatcherType
public enum DispatcherType {
FORWARD,
INCLUDE,
REQUEST,
ASYNC,
ERROR
}
- DispatcherType
- REQUEST : 클라이언트 요청
- ERROR : 오류 요청
- FORWARD : MVC에서 배웠던 서블릿에서 다른 서블릿이나 JSP를 호출할 때
- RequestDispatcher.forward(request, response);
- INCLUDE : 서블릿에서 다른 서블릿이나 JSP의 결과를 포함할 때
- RequestDispatcher.include(request, response);
- ASYNC : 서블릿 비동기 호출
- 필터와 DispatcherType
- LogFilter - DispatcherType 로그 추가
package hello.exception.filter; import lombok.extern.slf4j.Slf4j; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.util.UUID; @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) { throw e; } finally { log.info("RESPONSE [{}][{}][{}]", uuid, request.getDispatcherType(), requestURI); } } @Override public void destroy() { log.info("log filter destroy"); } }
- 로그를 출력하는 부분에 request.getDispatcherType() 을 추가해두었다
- WebConfig
- filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
- 이렇게 두 가지를 모두 넣으면 클라이언트 요청은 물론이고, 오류 페이지 요청에서도 필터가 호출된다.
- 아무것도 넣지 않으면 기본 값이 DispatcherType.REQUEST 이다. 즉 클라이언트의 요청이 있는 경우에만 필터가 적용된다.
- 특별히 오류 페이지 경로도 필터를 적용할 것이 아니면, 기본 값을 그대로 사용하면 된다.
- 물론 오류 페이지 요청 전용 필터를 적용하고 싶으면 DispatcherType.ERROR 만 지정하면 된다
package hello.exception;
import hello.exception.filter.LogFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
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;
}
}
서블릿 예외 처리 - 인터셉터
- LogInterceptor - DispatcherType 로그 추가
- 앞서 필터의 경우에는 필터를 등록할 때 어떤 DispatcherType 인 경우에 필터를 적용할 지 선택할 수 있었다. 그런데 인터셉터는 서블릿이 제공하는 기능이 아니라 스프링이 제공하는 기능이다. 따라서 DispatcherType 과 무관하게 항상 호출된다
- 대신에 인터셉터는 다음과 같이 요청 경로에 따라서 추가하거나 제외하기 쉽게 되어 있기 때문에, 이러한 설정을 사용해서 오류 페이지 경로를 excludePathPatterns 를 사용해서 빼주면 된다
- LogInterceptor - DispatcherType 로그 추가
package hello.exception.interceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;
@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);
}
}
}
- 필터는 DispatchType 으로 중복 호출 제거 ( dispatchType=REQUEST )
- 인터셉터는 경로 정보로 중복 호출 제거( excludePathPatterns("/error-page/")**
'Engineering WIKI > Spring Boot' 카테고리의 다른 글
[인프런 - 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술] 6장. 스프링 MVC - 기본 기능 (97) | 2023.09.27 |
---|---|
[인프런 - 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술] 5장. 스프링 MVC - 구조 이해 (140) | 2023.09.24 |
[인프런 - 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술] 4장. MVC 프레임워크 만들기 (80) | 2023.09.24 |
[인프런 - 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술] 3장.서블릿, JSP, MVC 패턴 (67) | 2023.09.22 |
[인프런 - 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술] 2장.서블릿 (105) | 2023.09.20 |
[인프런 - 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술] 1장.웹 애플리케이션 이해 (81) | 2023.09.07 |