본문 바로가기
Spring/MVC 2

8. 예외 처리와 오류 페이지

by wch_t 2024. 5. 5.

오류를 JSON이 아니라 HTML 화면으로 제공할 때, 단순히 4xx / 5xx 관련된 오류 화면만 보여주면 되기 때문에

이를 모두 구현해놓은 BasicErrorController를 사용하는 것이 편하다.

 


 

1. 서블릿 예외 처리

1) 자바 예외

public static void main(String[] args) 가 프로그램의 시작점으로, 'main' 이라는 이름의 쓰레드가 생성되어 실행된다.

이러한 main 쓰레드에서 예외가 발생하면, main 메서드를 넘어서 JVM까지 도달한다.

이 경우 JVM은 예외를 출력하고 main 쓰레드를 종료시킨다.

 

 

 

2) 웹 애플리케이션 예외

사용자의 각 요청은 별도의 쓰레드로 처리되고, 서블릿 컨테이너(Tomcat..)에서 관리된다.

예외가 발생했을 때, 애플리케이션 내에서 적절하게 try-catch 문을 통해 처리된 경우 프로그램이 정상적으로 실행이 된다.

하지만 애플리케이션 내에서 잡히지 않고 서블릿 컨테이너까지 전달되면, 서블릿 컨테이너는 몇 가지 절차를 따르게 된다.

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

 

 

1 - 해당 예외에 대한 처리 규칙이 있는지 확인한다.

(웹 애플리케이션의 web.xml 파일에는 특정 예외 타입에 대해 어떤 에러 페이지를 보여줄지 정의할 수 있다.)

(스프링부트에서는 web.xml 대신에 *서블릿 오류 페이지를 등록하면 된다.)

 

2 - 처리 규칙이 정의되어 있다면, 해당 규칙에 따라 사용자에게 적절한 응답을 반환한다.

 

3 - 처리 규칙이 정의되어 있지 않다면, 서블릿 컨테이너는 기본적인 에러 응답을 반환한다.

 

 


 

2. 서블릿 오류 페이지

컨트롤러에서 예외가 발생한 오류는 웹 애플리케이션 내에서 잡아내지 못하면 결국 WAS까지 전파가 된다.

이 때 WAS는 각 예외에 대한 처리 규칙을 다음과 같이 지정할 수 있다.

 

@Component
public class WebServerCustomizer implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {

    @Override
    public void customize(ConfigurableWebServerFactory factory) {

        // 404 NOT_FOUND가 발생하면 path 경로로 이동한다.
        ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-page/404");
        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);

    }
}

 

 

 

1) 오류 세부 정보

WAS에서 에러 페이지를 요청할 때, 이 때 request에는 에러에 대한 상세 정보들이 있어 Log로 출력할 수 있다.

 

@Slf4j
@Controller
public class ErrorPageController {

    //RequestDispatcher 상수로 정의되어 있음
    public static final String ERROR_EXCEPTION = "jakarta.servlet.error.exception"; // 예외 
    public static final String ERROR_EXCEPTION_TYPE = "jakarta.servlet.error.exception_type"; // 예외 타입
    public static final String ERROR_MESSAGE = "jakarta.servlet.error.message"; // 오류 메시지
    public static final String ERROR_REQUEST_URI = "jakarta.servlet.error.request_uri"; // 클라이언트 요청 URI
    public static final String ERROR_SERVLET_NAME = "jakarta.servlet.error.servlet_name"; // 오류가 발생한 서블릿 이름
    public static final String ERROR_STATUS_CODE = "jakarta.servlet.error.status_code"; // HTTP 상태 코드


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

    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));
        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()); // DispatcherType : Forward, Include, Request, Async, Error
    }
}

 

 

 

2) 문제점

1. WAS(여기까지 전파) ← 필터 ← 서블릿 ← 인터셉터 ← 컨트롤러(예외발생)
2. WAS('/error-page/404' 에러 페이지 요청) → 필터 → 서블릿 → 인터셉터 → 컨트롤러 → View

 

이러한 흐름에서 '/error-page/404' 에러 페이지 요청할 때 불필요하게 필터 / 인터셉터를 거치게 된다.

 

 


 

3. 스프링 부트 - 오류 페이지

위 과정에서는 예외 처리 페이지를 만들기 위해서

WebServerCustomizer에서 예외 종류에 따라서 ErrorPage를 만들어 호출할 경로를 지정한다.

그리고 예외 처리용 컨트롤러 ErrorPageController를 생성해야 한다.

 

스프링 부트는 이런 과정을 모두 기본으로 제공한다.

new ErrorPage("/error"), 상태코드와 예외를 설정하지 않으면 기본 오류 페이지를 사용한다.

 

 

 

1) 스프링 부트에서 기본으로 제공하는 기능

ErrorMvcAutoConfiguration BasicErrorController
Spring Boot 자동 구성 클래스 중 하나
BasicErrorController를 빈으로 등록
/error 경로에 매핑
Spring MVC에서 제공하는 기본 에러 컨트롤러

이를 상속받아 custom 에러 처리 로직 구현 가능

뷰 선택 우선 순위

1. 뷰 템플릿
resources/templates/error/500.html
resources/templates/error/5xx.html


2. 정적 리소스(static, public)
resources/static/error/400.html
resources/static/error/4xx.html


3. 적용 대상이 없을 때 뷰 이름(error)
resources/templates/error.html

 

 

 

 

2) BasicErrorController가 제공하는 기본 정보

 

BasicErrorController 는 다음의 오류 정보를 model에 담아서 view에 전달한다.

뷰 템플릿은 이 값을 활용해서 출력할 수 있다.

timestamp: Fri Feb 05 00:00:00 KST 2021
status: 400
error: Bad Request
exception: org.springframework.validation.BindException * trace: 예외 trace
message: Validation failed for object='data'. Error count: 1
errors: Errors(BindingResult)
path: 클라이언트 요청 경로 (`/hello`)

 

 

그러나 오류 관련 내부 정보를 사용자에게 노출하는 것은 보안상 문제가 될 수 있으므로,

오류 정보를 model에 포함할 지 여부를 선택할 수 있다. 

→ 운영 서버에서는 never를 사용하고, 오류는 log로 확인해야 한다.

// always : 항상 사용
// on_param : 파라미터가 있을 때 사용
// never : 사용하지 않음

server.error.include-exception=true
server.error.include-message=always
server.error.include-stacktrace=on_param
server.error.include-binding-errors=never

 

 


 

4. 서블릿 예외 - 필터 동작 처리

클라이언트에서 발생한 정상 요청인지, 오류 페이지를 출력하기 위한 WAS 내부 요청인지 구분할 수 있어야 한다.

이러한 각 요청의 유형 구분은 DispatcherType 으로 판단할 수 있다.

 

 

DispatcherType

- Request : 클라이언트 요청

- Error : 오류 요청

- Forward : 서블릿에서 다른 서블릿이나 jsp 요청

- Include : 서블릿에서 다른 서블릿이나 jsp 결과 포함 요청

- Async : 서블릿 비동기 요청

 

 

필터에서 에러 페이지일 경우 호출되지 않게끔, DispatcherType 요청 유형에 따라 필터를 적용 유무를 결정할 수 있다.

@Configuration
public class WebConfig implements WebMvcConfigurer {

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

        // 이 필터는 request, error 요청일 경우 호출된다.
        filterFilterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);

        return filterFilterRegistrationBean;
    }
    
}

 

 


 

5. 서블릿 예외 - 인터셉터 동작 처리

인터셉터는 DispatcherType과 무관하게 항상 호출된다.

대신 excludePathPateerns(...) 으로 요청 경로를 제외하기 쉽기 때문에, 이를 사용해서 오류 페이지 경로를 제외해주면 된다.

 

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor())
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns( // Filter 처럼 DispatcherType 에 따라 인터셉터 호출 유무를 결정할 수 없다.
                        "/css/**",
                        "/*.ico",
                        "/error", // 그 대신 오류 페이지 경로로 인터셉터 호출할 경로를 지정한다.
                        "/error-page/**"
                );
    }
    
}

 

'Spring > MVC 2' 카테고리의 다른 글

10. 스프링 타입 컨버터  (0) 2024.05.06
9. API 예외 처리  (0) 2024.05.06
7. 로그인 처리2 - 필터, 인터셉터  (0) 2024.05.04
6. 로그인 처리1 - 쿠키, 세션  (1) 2024.05.02
5. 검증 2 - Bean Validation  (0) 2024.05.01