스프링 공부 (인프런 김영한 선생님)/스프링 MVC 2편

[스프링 웹 MVC 2편] 012. 예외 처리

ProgYun. 2023. 8. 20. 14:25

서블릿 예외 처리

서블릿 예외 처리 방식

  • Exception
  • Response.sendError(HttpStatus.StatusCode, "ErrorMessage")

1. Exception

컨트롤러에서 발생한 예외는 인터셉터 -> 서블릿 -> 필터를 거쳐 WAS까지 반환됨

WAS로 예외가 올라오는 경우 Tomcat이 기본으로 제공하는 오류 화면을 볼 수 있다.
p.s) 이를 보기 위해 whitelabel 에러 페이지 생성 기능을 꺼두었다.

2. response.sendError(HttpStatus.상태코드, "errmsg")

HttpServletResponse가 제공하는 .sendError()메서드를 사용하는 방법도 있다.

서블릿 컨테이너에게 오류가 발생한 것을 전달할 수 있다

이 메서드는 HttpStatus.상태코드errmsg 즉 오류메세지도 전달할 수 있다.

사용 용례는 다음과 같다

package hello.exception.servlet;  

import lombok.extern.slf4j.Slf4j;  
import org.springframework.stereotype.Controller;  
import org.springframework.web.bind.annotation.GetMapping;  

import javax.servlet.http.HttpServletResponse;  
import java.io.IOException;  
@Slf4j  
@Controller  
public class ServletExController {  

    @GetMapping("/error-ex")  
    public void errorEx(){  
        throw new RuntimeException("Exception Occurred");  
    }  

    // WAS까지 예외가면 WAS에서 다시 여기로 호출함.  

    @GetMapping("/error-404")  
    public void error404(HttpServletResponse response) throws IOException {  
        // 발생하는 것은 아니고 메서드자체가 checked 예외로 IOException이 있어서 던져줘야함.  
        response.sendError(404, "404 오류!");  
    }  

    @GetMapping("/error-500")  
    public void error500(HttpServletResponse response) throws IOException {  
        // Exception 터진건 무조건 500으로 나간다.  
        // 아니면 직접 예외 상태코드를 아래처럼 만들어서 처리할 수 있다!  
        response.sendError(500);  
    }  
}

sendError 실행 과정

WAS (.sendError 호출 기록 확인) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러 (response.sendError)

WAS는 .sendError의 호출을 확인하면 설정된 오류코드에 맞추어 기본 오류페이지를 보여준다.


서블릿 예외처리 - 오류화면 제공

스프링 부트를 통해 서블릿 컨테이너를 실행하기 때문에 WebServerFactoryCustomizer 의 구현체를 생성하고 이를 @Component로 스프링 빈 등록함으로써 다음과 같이 서블릿 오류페이지를 등록할 수 있다.

// 서블릿 방식  
//@Component  
public class WebServerCustomizer // TOMCAT Server Error page Customization  
        implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {  
    @Override  
    public void customize(ConfigurableWebServerFactory factory) {  
        ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-page/404");  
        // errorPage404 호출
        ErrorPage errorPage500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error-page/500");  
        /// errorPage500 호출
        ErrorPage errorPageEx = new ErrorPage(RuntimeException.class, "/error-page/500");  
        // errorPageEx 호출

        factory.addErrorPages(errorPage404, errorPage500, errorPageEx);  
    }  


}

오류 페이지는 해당 예외와 자식예외까지 한꺼번에 처리한다.

내부 로직에 뭔가 isAssignableFrom 같은 supports 로직이 있을거같은데 그건 아니고 자바 Generic 이 이용됐다.

하긴 이건 Resolver 와는 느낌이 다르니,,,
(물론 뒤에 Exception 관련 Resolver들이 등장한다. 이건 애노테이션 기반인지, HTTP text/json인지 구분)

public ErrorPage(Class<? extends Throwable> exception, String path) {  
    this.status = null;  
    this.exception = exception;  
    this.path = path;  
}

위에서 특정 컨트롤러로 다시 호출하도록 코드를 구성했기 때문에 해당 urlPattern에 대응하는 컨트롤러를 구현해야한다.

@Slf4j  
@Controller  
public class ErrorPageController {  


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

그리고 리턴한 뷰에 대한 타임리프 템플릿을 이용한 html 페이지도 구성하였는데 별게 없어서 여기서는 다루지 않는다. (그냥 오류화면입니다 띄우는게 전부였기에)


서블릿 예외 처리 - 오류페이지 작동 원리

서블릿은 Exception 이 발생해서 서블릿 밖으로 전달되거나
Response.sendError()가 호출되었을 때 설정된 오류 페이지를 찾는다.

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

오류 정보 추가

WAS는 오류 페이지를 요청할 때 오류 정보를 Requestattribute에 추가해서 넘겨준다

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


    private void printErrorInfo(HttpServletRequest request){  
                log.info("Error Exception :{} ", request.getAttribute(ERROR_EXCEPTION)); // 예외
        log.info("Error Exception Type :{} ", request.getAttribute(ERROR_EXCEPTION_TYPE));  // 예외 타입
        log.info("Error Message :{} ", request.getAttribute(ERROR_MESSAGE)); // 오류 메세지 
        log.info("Error Request URI :{} ", request.getAttribute(ERROR_REQUEST_URI));  // 클라이언트 요청 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());
        // HTTP 상태 코드  
    }  


}

request.getDispatcherType()request.getAttribute(ERROR_EXCEPTION_CODE)이랑 다른게 뭔지 고민하고 있었는데 사실상 같은거였다.
위에 정의된 상수를 .getDispatcherType()에서 가져온거라...


서블릿 예외 처리 - 필터

일단 WAS가 예외를 감지하고 다른 페이지를 호출했다고 치자.

그런데, 에러로 인해 요청된 다음 페이지에서 필터 / 인터셉터 호출이 모두 다시 호출되는데

이 필터 인터셉터는 이미 이전에 컨트롤러에서 예외를 던지기 전 과정에서 모두 진행된 로직이다.

결국 불필요한 로직이 두 번 실행되는 것이고 이건 한번만 검증해줘도 될거같은데 방법이 없을까?

서블릿은 이런 문제를 해결하기 위해 이것이 고객의 요청인지, 아니면 WAS에서 오류 페이지를 호출한 것인지
DispatcherType이라는 추가 정보를 통해 구별한다.

DispatcherTypes

/*  
 * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements.  See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License.  You may obtain a copy of the License at * *     http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */
 * package javax.servlet;  

/**  
 * Enumeration of dispatcher types. Used both to define filter mappings and by Servlets to determine why they were * called. * * @since Servlet 3.0  
 */public enum DispatcherType {  

    /**  
     * {@link RequestDispatcher#forward(ServletRequest, ServletResponse)}  
     */    FORWARD,  

    /**  
     * {@link RequestDispatcher#include(ServletRequest, ServletResponse)}  
     */    INCLUDE,  

    /**  
     * Normal (non-dispatched) requests.     */    
            REQUEST,  

    /**  
     * {@link AsyncContext#dispatch()}, {@link AsyncContext#dispatch(String)} and  
     * {@link AsyncContext#addListener(AsyncListener, ServletRequest, ServletResponse)}  
     */    ASYNC,  

    /**  
     * When the container has passed processing to the error handler mechanism such as a defined error page.     */    
           ERROR  
}

다음과 같이 요청의 종류에 따라 DispatcherType을 구분한다.

그리고 WebMvcConfigurer의 구현체인 WebConfig에서 다음과 같이 DispatcherType에 따른 필터 실행 조건을 추가할 수 있다

setDispatcherTypes(...)

@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);  
        // 이 필터는 Request, Error인 경우 호출된다.  
        return filterRegistrationBean;  
    }  


}

서블릿 예외 처리 - 인터셉터

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

필터와는 다르게 인터셉터의 경우 스프링 MVC에 의해 제공되는 기능으로 서블릿이 제공하는 DispatcherType를 직접 사용할 수 없는데 때문에 WebConfig에서 excludPathPatterns를 사용하여 오류 페이지 경로에서는 인터셉터를 사용하지 않도록 설정해주어야 한다.

코드는 다음과 같다

@Override  
public void addInterceptors(InterceptorRegistry registry) {  
    registry.addInterceptor(new LogInterceptor())  
            .order(1)  
            .addPathPatterns("/**")  
            .excludePathPatterns("/css/**", "*.ico", "/error", "/error-page/**");  
    // 오류페이지 경로를 넣어버리면 된다 / DispatcherType을 가지고는 어떻게 할 수 없다  
    // provided by Spring MVC Not Servlet(DispatcherType - Servlet Functionality)  
}

정리

정상 요청 처리 과정
WAS -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러 -> 뷰

오류 요청 처리 과정

  1. WAS -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러
  2. 컨트롤러(예외) -> 인터셉터 -> 서블릿 -> 필터 -> WAS
  3. WAS에서는 .sendError() 호출 여부와 에러 페이지를 확인
  4. WAS(dispatcherType=error) -> 필터(x) -> 인터셉터(x) -> 컨트롤러 -> View 호출

오류 페이지 생성의 경우 내용이 너무 간단해서, 따로 정리하지는 않겠다.

만약 스프링 프로젝트를 하다가 오류페이지 관련해서 기억이 안나는게 있으면 교안을 다시 한번 보자(나를 위한 메모)