김영한 님의 스프링 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
를 상속받아 기능을 추가하면 된다