김영한 님의 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 강의를 보고 작성한 내용입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard
WAS ⬅ 필터 ⬅ 서블릿 ⬅ 인터셉터 ⬅ Controller( 예외 발생 )
예외 발생 시 위와 같은 과정으로 전파된다
서블릿은 아래 2가지 방식으로 예외 처리를 함
Exception ( 예외 ) : NPE와 같은 예외가 발생한 경우, WAS까지 예외가 전파
response.sendError() : 서블릿 컨테이너가 예외가 발생했다고 인지 가능
웹 어플리케이션에서는 사용자 요청 별로 Thread가 할당되고 서블릿 컨테이너 안에서 실행
어플리케이션에서 예외를 잡지 못하면 서블릿 밖까지 예외가 전달된다
( 서블릿을 넘어서 톰캣 같은 WAS까지 예외가 전달 )
WAS는 Exception이 발생하면 서버 내부에서 처리할 수 없는 오류가 발생한 것으로 생각해서 HTTP 상태 코드 500을 반환
위 메서드를 를 호출한다고 당장 예외가 발생하는 것은 아니지만, 서블릿 컨테이너에게 오류가 발생했다는 점을 전달할 수 있고 HTTP 상태 코드와 오류 메시지도 추가할 수 있다
response.sendError( HTTP 상태 코드 )
response.sendError( HTTP 상태 코드, 오류 메세지 )
위 메서드를 호출하면 response 내부에 오류가 발생했다는 상태를 저장
서블릿 컨테이너는 고객에게 응답하기 전에 response에 sendError()가 호출되었는지 확인
<web-app>
<error-page>
<error-code>404</error-code>
<location>/error-page/404.html</location>
</error-page>
<error-page>
<exception-type>java.lang.RuntimeException</exception-type>
<location>/error-page/500.html</location>
</error-page>
</web-app>
@Component
public class WebServerCustomizer implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
@Override
public void customize(ConfigurableWebServerFactory factory) {
ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-page/404");
ErrorPage errorPageEx = new ErrorPage(RuntimeException.class, "/error-page/500");
// 오류 페이지 등록
factory.addErrorPages(errorPage404, errorPage500, errorPageEx);
}
}
WebServerCustomizer : 기본 오류 페이지를 커스텀화하기 위해 사용
위처럼 작성하면 스프링부트가 실행될 때 톰캣에 오류 페이지를 등록
Exception을 등록하면 등록한 예외 뿐만 아니라 그 자식 타입의 예외들도 등록한 오류 페이지로 처리된다
오류 페이지를 등록한 후, 오류가 발생했을 때 처리할 수 있는 Controller가 필요
서블릿은 Exception이 발생해서 서블릿 밖으로 전달되거나 sendError()가 호출되었을 때 설정된 오류 페이지를 찾는다
@Controller
public class ErrorPageController {
@RequestMapping("/error-page/404")
public String errorPage404(HttpServletRequest request, HttpServletResponse response) {
log.info("errorPage 404");
return "error-page/404";
}
}
1. WAS ⬅ 필터 ⬅ 서블릿 ⬅ 인터셉터 ⬅ Controller ( 예외 발생 )
2. WAS에서
/error-page/404다시 요청 ➜ 필터 ➜ 서블릿 ➜ 인터셉터
➜ Controller (/error-page/404) ➜ View
서블릿은 Exception이 발생해서 서블릿 밖으로 전달되거나 sendError()가 호출되었을 때 설정된 오류 페이지를 찾는다
예외가 발생해서 WAS까지 전달된 후, 오류를 확인해서 ErrorPage 에 등록한 경로가 호출 ➜ WAS에서 다시 필터, 서블릿 등을 거쳐 ErrorPage 에 등록한 경로에 대해 처리하는 Controller 까지 다시 호출
예외 발생 ( ex> 404 ) ➜ WAS까지 전달
WAS에서 2-2에서 등록한 오류 페이지를 보고 발생한 예외에 맞는 경로를 요청
( ex> /error-page/404 )
WAS부터 다시 필터, 서블릿 등을 거쳐서 2-3의 Controller가 호출
( ex> @RequestMapping("/error-page/404") )
WAS 는 오류 페이지를 다시 요청할 뿐만 아니라 request.setAttribute()를 통해 오류에 대한 정보를 담아 Controller 에 넘겨준다
필요한 경우, 오류 페이지에 전달된 오류 정보를 사용할 수 있다
정보들은 RequestDispatcher에 상수로 정의되어 있다
public static final String ERROR_EXCEPTION = "javax.servlet.error.exception";서버가 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 내부에서 다시 한번 호출이 발생하면 필터, 서블릿, 인터셉터도 모두 다시 호출된다
but> 로그인 인증 체크의 경우, 이미 필터나, 인터셉터에서 로그인 체크를 완료했기 때문에 서버 내부에서 오류 페이지를 호출할 때 해당 필터나 인터셉트가 한번 더 호출되는 것은 매우 비효율적
그러므로 클라이언가 요청한 것인지, 아니면 오류 페이지를 출력하기 위한 서버 내부 요청인지 구분할 수 있어야 한다
➡️ 서블릿은 요청 구분을 위해 WAS에서 넘겨줄 때 DispatcherType라는 추가 정보 제공
DispatcherType 종류
REQUEST : 클라이언트 요청
ERROR : 오류 요청 ( 오류 페이지 요청을 위해 WAS가 다시 요청하는 경우 )
FORWARD : 서블릿에서 다른 서블릿이나 JSP를 호출할 때
( RequestDispatcher.forward(request, response); )
INCLUDE : 서블릿에서 다른 서블릿이나 JSP의 결과를 포함할 때
( RequestDispatcher.include(request, response);)
ASYNC : 서블릿 비동기 호출
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public FilterRegistrationBean logFilter() {
...
// 해당 필터는 아래 두 경우에 호출된다
filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
return filterRegistrationBean;
}
}
필터는 DispatcherType을 지정해서 중복 호출을 제거할 수 있다
setDispatcherTypes()에 지정된 DispatcherType의 경우에만 필터가 호출된다
위의 예시의 경우 클라이언트 요청과 오류 페이지 요청 둘 다 필터가 호출
아무것도 설정하지 않으면 DispatcherType.REQUEST가 기본값
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/**", "*.ico", "/error", "/error-page/**");
}
}
인터셉터는 필터처럼 DispatcherType을 지정할 수 없기 때문에 경로 정보로 중복 호출을 제거한다
등록할 때 setDispatcherTypes()같은 메서드가 없기 때문에 excludePathPatterns() 에서 지정해야함
excludePathPatterns() 에 추가하면 오류 발생 시의 로그를 출력하지 않는다WebServerCustomizer 생성
예외 종류에 따라 ErrorPage 추가
예외 처리용 Controller 생성
스프링부트는 위의 과정을 모두 기본으로 제공
ErrorPage 자동 등록
/error 경로로 기본 디폴트 오류 페이지를 설정
new ErrorPage("/error") 가 자동으로 등록
상태 코드와 예외를 설정하지 않으면 기본 오류 페이지로 사용
서블릿 밖으로 예외가 전달되거나, response.sendError()이 호출되면 모든 오류는 /error를 호출
BasicErrorController라는 스프링 컨트롤러 자동 등록
/error을 매핑해서 처리하는 Controller즉, 아무것도 설정하지 않았을 때 오류가 발생하면 오류 페이지로 /error를 요청하고 스프링부트가 자동으로 등록한 BasicErrorController는 /error 경로를 기본으로 받는다
BasicErrorController가 처리하는 순서
뷰 템플릿 : resources/templates에 있는 html
정적 리소스 : resources/static에 있는 html
적용 대상이 없는 경우 : resources/templates/error.html
같은 우선순위라면 구체적인 파일명이 우선순위가 더 높다
ex> 4XX.html보다 404.html이 우선순위가 더 높다
ex> 404.html와 4XX.html이 존재 ➜ 404 오류 발생 ➜ 404.html을 오류 페이지로 사용
400번대 오류가 발생하면 4XX.html을 실행하도록 컨트롤러가 작성되어 있다
ex> 404.html와 4XX.html이 존재 ➜ 400 오류 발생 ➜ 4XX.html을 오류 페이지로 사용
ex> 404.html이 없고 4XX.html이 존재 ➜ 404 오류 발생 ➜ 4XX.html을 오류 페이지로 사용
BasicErrorController는 몇몇 정보들을 model에 담아서 view에 전달하기 때문에 view template에서 이 값을 활용해 동적 HTML 오류페이지를 만들 수 있다
but> 오류 관련 내부 정보들을 고객에게 노출하는 것은 보안에 문제가 될 수도 있다
application.properties에서 BasicErrorController가 오류 정보를 model 에 포함할지 여부 선택할 수 있다
on_param : url에 특정 (쿼리)파라미터가 있을 때만 정보를 제공 ( ?message= 가 있으면 message에 관련된 정보를 제공 )server.error.whitelabel.enabled=true : 오류 처리 화면을 찾지 못했을 경우, 스프링 whitelabel 오류 페이지 적용
server.error.path=/error : 오류 페이지 경로, 스프링부트가 자동 등록하는 서블릿 글로벌 오류 페이지 경로와 BasicErrorController 오류 컨트롤러 경로에 함께 사용
오류를 공통으로 처리하는 컨트롤러의 기능을 변경하고 싶다면 ErrorController 인터페이스를 상속받아 구현하거나 BasicErrorController를 상속받아 기능을 추가하면 된다