Java - Spring &&n SpringBoot

Spring - 예외 상황 처리 1) Servlet 예외 처리 : 404 error, 500 error

TerianP 2022. 8. 19.
728x90

1. 서버 예외 처리

  • 서버를 다루면서 다양한 에러를 만나게 된다. 특히나 서버단에서 프로그래밍을 잘못하면 404, 500 등등 엄청나게 다양한 오류를 클라이언트는 마주할 수 밖에 없다.
  • 이때 이런 오류에 맞춰서 오류에 대한 예외처리 - 404 페이지, 500 오류 처리 등 - 을 할 수 있는 방법이 있다.
  • 예외처리는 서블릿으로 예외 처리하는 방법과 스프링 부트를 통해 예외 처리를 하는 방법 2가지가 있다.

 

2. 웹 어플리케이션의 예외처리

  • 웹 어플리케이션은 사용자 요청별로 별도의 쓰레드가 할당되고 서블릿 컨테이너 안에서 실행된다
  • 이때 에플리케이션 안에서 예외가 발생하고 이를 try ~ catch 로 잡아서 처리하면 아무런 문제가 없다.

 

2) WAS 가 자체적으로 처리하도록 만들기 : 정확히는 Spring 의 오류 기본 페이지 출력하기!

  • 컨트롤러에서 예외가 발생한 경우 WAS 까지 올라가게 되고 이에 맞는 페이지를 반환한다.
    • 즉 컨트롤러의 예외가 거슬러 올라가서 최종적으로 WAS 까지 전파되고 이에 맞는 예외 페이지가 보여진다 —> 500에러 발생
WAS(여기까지 전파) ← 필터 ← 서블릿 ← 인터셉터 ← 컨트롤러(예외발생)

그페이지를 찾지 못하는 경우 → (그 유명한) 404 에러

 

2) HttpServletResponse 의 sendError 메서드 사용하기

  • sendError 메서드를 사용하면 당장 예외가 발생하는 것은 아니지만, 예외가 발생했을 때 즉 error 이 발생했을 때 서블릿 컨테이너에게 에러 상태 코드와 메시지를 담아서 전달할 수 있다.
  • 아래와 같은 순서로 컨트롤러에서 WAS 로 전달된다.
WAS(sendError 호출 기록 확인) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(response.sendError())
  • response.sendError() 를 호출하면 response 내부에는 오류가 발생했다는 상태를 저장해둔다. 그리고 서블릿 컨테이너는 고객의 응답전에 response 에 sendError() 가 호출되었는지 확인한다. 만약 sendError 이 호출되었다면 설정한 오류 코드에 맞추어 기본 오류 페이지를 보여준다.

 

3. 서블릿 예외 처리 : 오류 화면 제공

1) WebServerCustomizer : WebServerFactoryCustomizer 구현

package HJproject.Hellospring.ServletException;

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

// WebServerFactoryCustomizer<ConfigurableWebServerFactory> 인터페이스를 구현하여 커스텀 에러 페이지를 설정할 수 있다
// 만약 404, 500, runtimeException 등이 발생한다면
// WAS 에서는 ErrorPage 에서 설정한 HttpStatus 코드와 함께 Path 위치로 가도록 다시 http 요청을 실행한다.
// 따라서 path 에 해당하는 요청을 처리할 컨트롤러가 필요하다

@Component
public class WebServerCustomizer implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
    @Override
    public void customize(ConfigurableWebServerFactory factory) {
        // 404 error
        ErrorPage error404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-page/404");

        // 500 error
        ErrorPage error500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error-page/500");

        // runtime Exception 및 runtime Exception 의 자식 예외도 포함
        ErrorPage exPage = new ErrorPage(RuntimeException.class, "/error-page/500");

        factory.addErrorPages(error404, error500, exPage);
    }
}

 

2) ErrorPage : errorPage 컨트롤러

package HJproject.Hellospring.ServletException;

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

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Slf4j
@Controller
public class ErrorPageController {

    @RequestMapping("/error-page/404")
    public String error404(HttpServletRequest req, HttpServletResponse resp) {
log.info("errorPage 404");
        printErrorInfo(req);
        return "error-page/404";
    }

    @RequestMapping("/error-page/500")
    public String error500(HttpServletRequest req, HttpServletResponse resp) {
log.info("errorPage 500");
        printErrorInfo(req);
        return "error-page/500";
    }

    private void printErrorInfo(HttpServletRequest req) {
log.info("dispatchTypes= {}", req.getDispatcherType());
    }
}

 

코드 확인 : 커스텀 에러 페이지

내가 만든 커스텀 에러 페이지가 똭!!

이쁜 404
더 이쁜 500


4. 서블릿 예외 처리에 따른 중복 호출 해결하기

  • 서블릿 예외 처리에 따라서 필터를 사용하게 되면 중복 호출이라는 문제가 발생한다.
  • 예를 들어 로그인 인증 체크의 경우 유저가 서버에 접소해서 로그인 인층 체크 페이지로 들어가는 순간 필터나 인터셉터를 통해 한번 로그인 체크를 완료한다.그러나 서버 내부에서 오류가 발생하면 이런 오류 페이지를 호출했을 때 다시 필터나 인터셉터가 호출되기 때문이다.
    • 대표적으로 아래와 같은 경우
1. WAS(에러 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예약 발생) <- 에러 발생
2. 전파된 에러를 받고 WAS 가 '/error-page/500' 다시 요청 -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러(/error-page/500) -> view
  • 이러한 필터나 인터셉터의 중복 호출 문제를 해결하기 위해서는 클라이언트로부터 발생한 정상 요청인지 아니면 오류 페이지를 출력하기 위한 내부 요청인지 구분할 수 있어야 한다.

 

1) DispatcherType

  • 필터는 이러한 중복 호출을 해결하기 위해 DispatcherType 이라는 옵션을 제공한다.
  • 정확히는 request.getDispatcherType() 을 사용해서 가져올 수 있다.
  • 아래는 DispathcerType enum
public enum DispatcherType {

FORWARD, // MVC 에서 서블릿에서 다른 서블릿이나 jsp 를 호출할 때??

INCLUDE, // 서블릿에서 다른 서블릿이나 jsp 결과를 포함할 때 사용됨

REQUEST, // 클라이언트의 요청

ASYNC, // 서블릿 비동기 호출

ERROR // 오류 요청
}

 

 

2) 중복 호출 확인하기 : ServletFilter

먼저 실제로 Filter 가 중복 호출이 되는지 확인해보자

  • SpringConfig
@Bean
    public FilterRegistrationBean logFilterDispatcher(){
        FilterRegistrationBean<Filter> filter = new FilterRegistrationBean<>();
        filter.setFilter(new LogFilterException());
        filter.setOrder(1);
        filter.addUrlPatterns("/*");
        // 이 필터는 DispatcherType 이 REQUEST 와 ERROR 인 경우에만 호출된다!!
        filter.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
        return filter;
    }
  • ServletFilter
package HJproject.Hellospring.ServletException;

import lombok.extern.slf4j.Slf4j;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.UUID;

@Slf4j
// DispatcherType 을 확인하기 위한 logFilter
public class LogFilterException implements Filter{
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("log filter init");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.info("log filter doFilter");

        HttpServletRequest req = (HttpServletRequest) request;

        String uri = req.getRequestURI();
        String uuid = UUID.randomUUID().toString();

        try {
            // Dispatcher 을 확인하기 위해 req.getDispatcherType 으로 가져온다
            log.info("REQUEST [{}][{}][{}]", uuid, uri, req.getDispatcherType());

            chain.doFilter(request, response);
        } catch (Exception e) {
            throw e;
        }finally {
            log.info("RESPONSE [{}][{}]", uuid, uri);
        }
    }

    @Override
    public void destroy() {
        log.info("log filter destory");
    }
}

 

코드 확인 : 필터 중복 호출

REQUEST 와 RESPONSE 이 2번씩 불러오는 걸 확인 할 수 있다. 즉 서블릿 필터가 총 2번 호출된다.

이는 아까 이야기했듯이 처음에 클라이언트의 요청에 따라서 서블릿 필터를 거쳐서 컨트롤러까지 도착한다. 그 후 컨트롤러에서는 에러를 뱉어내고, 에러를 들고! WAS 까지 진행한다.

WAS 는 에러를 확인하고 다시 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러까지 도착한다. 컨트롤러는 해당 오류를 확인 후 /error-page/500 으로 응답을 보내게 된다.

 


3) 중복 호출 제외하기

  • 중복 호출을 제거하는 방법은 사실 아주 간단하다. SpringConfig 에서 setDispatcherType 부분만 조정해주면된다.
  • 정확히는 이 옵션을 완전히 제거해버리면 된다. 이렇게 하면 서블릿 필터의 기본값은 DispatcherType.REQUEST 인 상태, 즉 클라이언트의 정상 요청일 때만 불러오기 때문에 DispatcherType.ERROR 인 상태에서는 필터가 호출되지 않기 때문이다.
@Bean
    public FilterRegistrationBean logFilterDispatcher(){
        FilterRegistrationBean<Filter> filter = new FilterRegistrationBean<>();
        filter.setFilter(new LogFilterException());
        filter.setOrder(1);
        filter.addUrlPatterns("/*");
        // 아래 설정을 안하면 기본값!! DispatcherType.REQUEST 인 경우만 필터가 호출된다
        // filter.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
        return filter;
    }

 

Dispatcher.REQUEST 상태, 한번만 호출된다!

 


- Reference

HTML 404 Error Page Templates - DevBeep

 

HTML 404 Error Page Templates - DevBeep

We have discussed about HTML 404 Page Templates in this post. Follow this post to know more about HTML 404 Page Templates for your websites.

devbeep.com

 

500 Error Page HTML Templates

 

500 Error Page HTML Templates

Collection of free 500 error page HTML templates from codepen and other resources. Update of January 2020 collection. 13 new items.

freefrontend.com

 

댓글