Spring

오류화면 보여주기

potatoo 2023. 8. 3. 21:51
728x90

서블릿 자체 예외 처리

@Slf4j
@Controller
public class ServletExController {

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

    @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);
    }
}

서블릿 컨테이너가 제공하는 기본 예외 처리를 터트리면 500 error를 던져주고, 서블릿의 sendError를 사용하면 개발자가 직접 예외 코드를 지정할 수 있다.

 

서블릿은 Exception이 발생해서 서블릿 밖으로 전달되거나 response.sendError()가 호출 되었을 때 각각의 상황에 맞는 오류 처리 기능을 제공한다.

 

과거에는 web.xml이라는 파일에 오류화면을 등록했다.

 

서블릿 컨테이너가 제공하는 기본 예외처리 화면대신 직접 만든 예외페이지를 보여주기 위해서는 WebServerFactoryCustomizer<ConfigurableWebServerFactory>를 사용하면 된다.

@Component
public class WebServerCustomizer implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
    @Override
    public void customize(ConfigurableWebServerFactory 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);
    }
}

예외에 따른 라우팅을 설정해준다.

라우팅 설정에 따라 예외 페이지 컨트롤러를 작성한다.

@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";
    }
}

이렇게 되면 맨 위에 코드에서 RuntimeException이 발생했을 때 WebServerCustomizer에서 생성한 에러 페이지에서 /error-page/500을 호출하고 ErrorPageController에서 라우팅한 예외처리 화면(error-page/500)을 띄워준다.

 

다시 말하면, WAS는 해당 예외를 처리하는 오류 페이지 정보를 확인한다.

new ErrorPage(RuntimeException.class, "/error-page/500")

예를 들어서 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는 오류 페이지 경로를 찾기위해 오류 페이지 경로로 필터, 서블릿, 인터셉터, 컨트롤러가 모두 다시 호출한다.

 

오류 정보 추가

WAS는 오류 페이지를 단순히 다시 요청만 하는 것이 아니라. 오류 정보를 request의 attribute에 추가해서 넘겨준다.

필요하면 오류 페이지에서 전달된 오류 정보를 사용할 수 있다.

 

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";

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 상태 코드

 

 

DispatcherType

오류가 발생하면 오류 페이지를 출력하기 위해 WAS 내부에서 다시 한번 호출이 발생한다. 이때 필터, 서블릿, 인터셉터도 모두 다시 호출된다. 그런데 로그인 인증 체크 같은 경우를 생각하면, 이미 한번 필터나, 인터셉터에서 로그인 체크를 끝냈다. 서버 내부에서 오류 페이지를 호출한다고 해서 해당 필터나 인터셉트가 한번 더 호출되는것은 비효율이다.

결국 클라이언트로부터 발생한 정상 요청인지, 아니면 오류 페이지를 출력하기 위한 내부 요청인지 구분할 수 있어야 한다.

서블릿은 이런 문제 해결을 위해 DispatchType이라는 추가 정보를 제공한다.

 

오류 정보 추가에 로그를 추가하면

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

dispatchType = ERROR로 나오는 것을 확인할 수 있다.

 

고객이 처음 요청하면 dispatcherType=REQUEST이다.

서블릿 스펙은 실제 고객이 요청한 것인지, 서버가 내부에서 오류 페이지를 요청하는 것인지 DispatherType으로 구분할수 있는 방법을 제공한다.

public enum DispatcherType{
    FORWARD,
    INCLUDE,
    REQUEST,
    ASYNC,
    ERROR
}

REQUEST : 클라이언트 요청

ERROR : 오류 요청

FROWARD : 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.ERROR,DispatcherType.REQUEST);
        return filterFilterRegistrationBean;
    }
}

이렇게 두 가지를 모두 넣으면 클라이언트 요청은 물론이고, 오류 페이지 요청에서도 필터가 호출된다.

아무것도 넣지 않으면 기본 값이 DispatcherType.REQUEST이다. 즉 클라이언트의 요청이 있는 경우에만 필터가 적용된다. 특별히 오류 페이 경로도 필터를 적용할 것이 아니면, 기본값을 그대로 사용하면 된다.

 

전체 흐름 정리

정상 요청

WAS(dispatchType=REQUEST)->필터->서블릿->인터셉터->컨트롤러->View

오류 요청

필터는 DispatchType으로 중복 호출 제거(dispatchType=REQUEST)

인터셉터는 경로 정보로 중복 호출 제거(excludePathPatterns("error-page/**")

더보기

1. WAS(/error-ex, dispatchType=REQUEST) -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러

2. WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외 발생)

3. WAS 오류 페이지 확인

4. WAS(/error-page/500, dispatch=ERROR) -> 필터(x) -> 서블릿 -> 인터셉터(x) -> 컨트롤러(/error-page/500) -> View

 

728x90