본문 바로가기
SSR/Servlet & JSP

서블릿 예외 페이지

by oncerun 2022. 1. 13.
반응형

스프링이 아닌 서블릿 컨테이너는 예외를 어떤 방식으로 처리할까?

 

  • Exception(예외) 
    서블릿에서 로직을 처리하다 실제 우리가 접하는 Exception이 발생했을 때 WAS까지 예외가 전달되면서 처리
  • response.sendError(HTTP 상태 코드, 오류 메시지)

 

 

Exception

 

java의 main()를 실행하는 경우 main이라는 이름의 스레드가 실행됩니다. 실행 도중에 예외를 catch하지 못하고 처음 실행한 main() 메서드를 넘어서 예외가 던져지면, 예외 정보를 남기고 해당 스레드는 종료된다.

 

웹 애플리케이션

 

사용자 요청 별로 별도의 스레드가 할당이 되고, 해당 스레드가 서블릿 컨테이너 안에서 서블릿의 실제 코드를 실행한다. 

웹 애플리케이션에서 예외가 발생했는데, try-catch로 예외를 잡아서 처리하면 문제가 발생되지 않지만, 만약 애플리케이션에서 예외를 잡지 못하고, 서블릿 밖으로 예외가 전달되면 어떻게 동작될까?

 

@GetMapping("/error")
public void error(){
    throw new RuntimeException("예외 발생");
}

예외를 요청하게 되면 status 500으로 톰캣의 기본 제공 오류 화면을 볼 수 있다.

즉 서버에서 처리할 수 없는 예외로 인지하고 HTTP STATUS 500- Internal Server Error로 처리해 준다.

 

 

response.sendError( status_code, message)

오류가 발생했을 때 HttpServlerResponse가 제공하는 sendError()를 사용해도 된다.  이 메서드를 호출하는 즉시 예외가 발생하는 것은 아니지만, 서블릿 컨테이너에게 오류가 발생했다는 것을 전달할 수 있다.

이 메서드를 사용 시 Arguments로 HTTP 상태 코드와 오류 메시지를 같이 전달할 수도 있다.

 

@GetMapping("/error-404")
public void error404(HttpServletResponse response) throws IOException {
    response.sendError(HttpStatus.NOT_FOUND.value(), "해당 페이지를 찾지못하는 404에러");
}

 

이는 서블릿 컨테이너가 response 내부에 오류가 발생한 상태를 확인한 후 sendError()가 호출되었다면 오류가 발생한 것으로 취급하여 오류 코드에 맞추어 기본 오류 페이지를 보여준다.

 

 

서블릿 컨테이너의 기본 오류 화면보다는 의미 있는 오류 화면이 좋다. 

과거, 아니 나에겐 현재인데(구해줘...) web.xml에서 오류 화면을 등록했다.

<web-app>
        <error-page>
		<exception-type>java.lang.Throwable</exception-type>
		<location>/com/error.jsp</location>
	</error-page>
	
	<error-page>
		<error-code>404</error-code>
		<location>/com/404.jsp</location>
	</error-page>
	
	<error-page>
		<error-code>500</error-code>
		<location>/com/500.jsp</location>
	</error-page>
</web-app>

 

이는 exception-type에 따라 보여줄 화면과, HTTP 상태 코드에 따른 별도의 오류 화면을 지정했다.

 

서블릿 3.0 부터는 web.xml 없는 개발 및 배포가 가능해졌고 요즘에는 스프링 부트를 통해서 서블릿 컨테이너를 실행하기 때문에 스프링 부트가 제공하는 기능을 사용해서 서블릿 오류 페이지를 등록하면 된다.

 


import org.springframework.boot.web.server.ErrorPage;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

@Component
public class WebServerCustomizer implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {


    @Override
    public void customize(ConfigurableServletWebServerFactory factory) {

        ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-page/400");
        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);

    }
}

웹 서버에 대한 설정을 변경하기 위한 스펙은 지정되어 있기 때문에 다음과 같이 설정해야 합니다.

 

ErrorPage객체를 만들고 ConfigurableServletWebServerFactory에 추가하는 방식입니다. Type으로 추가한 경우 자식 에러까지 전부 적용됩니다.

 

이때의 흐름은 다음과 같습니다.

클라이언트 -> WAS -> 필터 ->  서블릿 -> 인터셉터 -> 컨트롤(예외 발생)-> 
인터셉터 -> 서블릿 -> 필터 -> WAS(예외 확인) ->
필터 -> 서블릿 -> 인터셉터 -> 컨트롤(예외 처리 컨트롤러) ->
인터셉터 -> 서블릿 -> 필터 -> WAS -> 클라이언트

 

인터셉터는 스프링 프레임워크를 사용하는 경우입니다.

 

 WAS는 오류 페이지를 다시 요청하는 것이 아니라, 오류 정보를 request의 attribute에 추가해서 넘겨준다. 필요하면 해당 정보를 사용할 수 있다.

 

이러한 정보는 RequestDispatcher에 상수로 정의가 되어 있다.

public interface RequestDispatcher {

    /**
     * @since Servlet 3.0
     */
    static final String FORWARD_REQUEST_URI = "javax.servlet.forward.request_uri";

    /**
     * @since Servlet 3.0
     */
    static final String FORWARD_CONTEXT_PATH = "javax.servlet.forward.context_path";

    /**
     * @since Servlet 4.0
     */
    static final String FORWARD_MAPPING = "javax.servlet.forward.mapping";

    /**
     * @since Servlet 3.0
     */
    static final String FORWARD_PATH_INFO = "javax.servlet.forward.path_info";

    /**
     * @since Servlet 3.0
     */
    static final String FORWARD_SERVLET_PATH = "javax.servlet.forward.servlet_path";

    /**
     * @since Servlet 3.0
     */
    static final String FORWARD_QUERY_STRING = "javax.servlet.forward.query_string";

    /**
     * @since Servlet 3.0
     */
    static final String INCLUDE_REQUEST_URI = "javax.servlet.include.request_uri";

    /**
     * @since Servlet 3.0
     */
    static final String INCLUDE_CONTEXT_PATH = "javax.servlet.include.context_path";

    /**
     * @since Servlet 3.0
     */
    static final String INCLUDE_PATH_INFO = "javax.servlet.include.path_info";

    /**
     * @since Servlet 4.0
     */
    static final String INCLUDE_MAPPING = "javax.servlet.include.mapping";

    /**
     * @since Servlet 3.0
     */
    static final String INCLUDE_SERVLET_PATH = "javax.servlet.include.servlet_path";

    /**
     * @since Servlet 3.0
     */
    static final String INCLUDE_QUERY_STRING = "javax.servlet.include.query_string";

    /**
     * @since Servlet 3.0
     */
    public static final String ERROR_EXCEPTION = "javax.servlet.error.exception";

    /**
     * @since Servlet 3.0
     */
    public static final String ERROR_EXCEPTION_TYPE = "javax.servlet.error.exception_type";

    /**
     * @since Servlet 3.0
     */
    public static final String ERROR_MESSAGE = "javax.servlet.error.message";

    /**
     * @since Servlet 3.0
     */
    public static final String ERROR_REQUEST_URI = "javax.servlet.error.request_uri";

    /**
     * @since Servlet 3.0
     */
    public static final String ERROR_SERVLET_NAME = "javax.servlet.error.servlet_name";

    /**
     * @since Servlet 3.0
     */
    public static final String ERROR_STATUS_CODE = "javax.servlet.error.status_code";

이러한 오류 정보를 로그 파일로 남기거나 로그를 찍으면 오류 분석에 큰 도움을 줄 수 있다.

 

 

 

클라이언트 -> WAS -> 필터 ->  서블릿 -> 인터셉터 -> 컨트롤(예외 발생)-> 
인터셉터 -> 서블릿 -> 필터 -> WAS(예외 확인) ->
필터 -> 서블릿 -> 인터셉터 -> 컨트롤(예외 처리 컨트롤러) ->
인터셉터 -> 서블릿 -> 필터 -> WAS -> 클라이언트

예외 발생과 오류 페이지 요청 흐름을 보면  필터, 서블릿 , 인터셉터가 여러 번 호출되는 경우가 있다.

서버 내부에서 오류 페이지를 호출한다고 해서 필터, 인터셉터가 계속 호출되는 경우에는 매우 비효율적이다.

 

결국 클라이언트로부터 발생한 정상 요청인지, 내부 요청인지 구분할 수 있어야 하는데, 서블릿은 이러한 문제를 해결하기 위해 DispatcherType이라는 추가 정보를 제공한다.

 

 

DispatcherType

log.info("dispatchType={}", request.getDispatcherType());

/**
 * @since Servlet 3.0
 */
public enum DispatcherType {
    FORWARD,
    INCLUDE,
    REQUEST,
    ASYNC,
    ERROR
}

 

  • REQUEST : 클라이언트 요청
  • ERROR : 오류 요청
  • FORWARD : MVC에서 배웠던 서블릿에서 다른 서블릿이나 JSP를 호출할 때 RequestDispatcher.forward(request, response);
  • INCLUDE : 서블릿에서 다른 서블릿이나 JSP의 결과를 포함할 때 RequestDispatcher.include(request, response);
  • ASYNC : 서블릿 비동기 호출

 

@Configuration
public class WebConfig implements WebMvcConfigurer {


    @Bean
    public FilterRegistrationBean logFilter(){
        FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
        filterFilterRegistrationBean.setFilter(new LogFilter());
        filterFilterRegistrationBean.setOrder(1);
        filterFilterRegistrationBean.addUrlPatterns("/*");
        filterFilterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);

        return filterFilterRegistrationBean;
    }

}

필터의 경우 FilterResgistartionBean의 setDispatcherTypes를 통해 필터를 실행할 타입을 지정하면 

해당 타입의 호출에만 Filter가 호출된다. default 설정은 REQUEST만 설정된다.

그렇기 때문에 기본 값에 따라서 내부 호출인 경우에는 필터가 작동하지 않는다.

 

인터셉터는 제외할 대상을 설정에서 excludePathPatterns으로 처리한다.

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new LogInterceptor())
            .order(1)
            .addPathPatterns("/**")
            .excludePathPatterns("/css/**", "*.ioc", "/error", "/error-page/**");

}
반응형

'SSR > Servlet & JSP' 카테고리의 다른 글

서블릿 3.0 파일 업로드  (0) 2020.05.30
Filter  (0) 2020.05.24
DBCP를 이용해서 커넥션 풀 사용하기  (0) 2020.05.20
JSP SCOPE  (0) 2020.05.13
HttpServletRequest/Response  (0) 2020.05.09

댓글