예외처리와 오류페이지

민지·2024년 6월 21일
0

SpringMVC2

목록 보기
5/6

핵심

서블릿이 예외를 처리하는 원리를 알아보고,
실제로 스프링부트에서 예외가 발생하면 어떻게 오류페이지를 만드는지 정리해보았다.

1. 서블릿 - 예외처리
	1-1. Exception 발생시키는 방법
    1-2. 서블릿 - 오류 화면 제공하는 방법
    1-3. 서블릿 - 오류페이지 작동원리
    1-4. 서블릿 - 필터 중복 호출 방지 (DispatcherType)
    1-5. 스프링 - 인터셉터 중복 호출 방지

2. 스프링 - 오류페이지 만드는 법

1. 서블릿 - 예외처리

1-1. Exception 발생시키는 방법

  1. 잘못된 요청 전송
    ex) 존재하지 않는 URL 접근 -> 404 에러
    인증하지 않음 -> 401 에러
    서버 내부 에러 -> 500 에러

    이렇듯, 잘못된 요청을 전송하여 에러를 발생시킬 수 있음

  2. throw로 직접 예외 던져주기

    //  ServletExcotroller - 서블릿 예외 컨트롤러
    @Slf4j
    @Controller
    public class ServletExController {
       @GetMapping("/error-ex")
       public void errorEx() {
       throw new RuntimeException("예외 발생!");
     }
    }

    /error-ex url로 들어가면, throw로 런타임에러를 발생시켰다. 해당 사이트에 들어가면 HTTP 상태코드 500을 반환하는 것 확인할 수 있다.

  3. HttpServletResponse에 에러를 담아서 전송

    @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);
    }
    
    -- response.sendError(HTTP 상태코드)
    -- response.sendError(HTTP 상태코드, 오류메세지)
    WAS(sendError 호출 기록 확인) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러
    (response.sendError())
    
    결론부터 말하자면,
    예외가 발생했을 때 각 단계에서 예외를 처리하지 못하고 던져지면, 최종적으로 WAS가 예외처리해줌.
    
    이때, sendError 메서드를 확인해, 클라이언트에게 오류 response를 보내는 흐름.

    response.sendError()를 호출하면, response 내부에 오류가 발생했다는 상태를 저장해둔다. (당장 예외발생 X)
    그리고 서블릿 컨테이너는 고객에게 응답 전에 response에 sendError()가 호출되었나 확인한다.

    그리고 호출이 되었다면, 설정한 오류코드에 맞춘 오류페이지를 보여준다. (개발자가 지정한 것이 없다면, tomcat 기본 제공 오류페이지를 보여줌)


1-2. 서블릿 - 오류 화면 제공하는 방법

서블릿이 제공해주는 기본오류화면 대신 개발자가 직접 오류화면을 만들어서 보여주고 싶다면, application.properties에서 톰켓 기본 오류화면제공 설정을 꺼주어야 한다.

application.properties

server.error.whitelabel.enabled=false

서블릿의 오류페이지를 만드는 방법은 3가지 일을 해주면 된다.

  1. 서블릿 오류페이지 등록

    • WebServerFactoryCustomizer 인터페이스 구현
  2. 오류를 처리할 컨트롤러 등록

  3. 오류처리 View 화면 만들기

서블릿 오류페이지 등록

@Component
public class WebServerCustomizer implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
    @Override
    public void customize(ConfigurableWebServerFactory factory) {

        // NOT_FOUND 에러가 발생하면, /error-page/404 컨트롤러를 호출해라 !!
        ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-page/404");
        ErrorPage errorPage500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error-page/500");

        // RuntimeException 또는 그 자식타입의 예외까지 발생하면, /error-page/500 컨트롤러 호출해라 !!
        ErrorPage errorPageEx = new ErrorPage(RuntimeException.class, "/error-page/500");

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

오류를 처리할 컨트롤러 등록

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

오류처리 View 화면

컨트롤러에서 리턴해주는 위치에다가 뷰 html 화면을 만들어주면 됨.
위 예시같은 경우,
/templates/error-page/404.html
/templates/error-page/500.html
이 경로에 해당함.


1-3. 서블릿 - 오류페이지 작동원리

서블릿에서 예외가 발생되고, WAS까지 예외가 전파되면,

  • WAS에서 예외발생여부를 확인하고
  • 개발자가 설정한 오류처리화면이 있다면 그 화면을 보여주는 방법

을 배웠다.

이 내용의 흐름을 다시 정리해보자.

예외 / sendError 발생 흐름

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

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

예를 들어서, RuntimeException 에러가 발생해서 WAS까지 전달되면, WAS는 설정된 오류페이지 정보를 확인한다.

확인해보니, RuntimeException의 오류페이지로 /error-page/500이 지정되어 있다.

WAS는 오류페이지를 출력하기 위해, /error-page/500을 다시 요청한다.

오류페이지 요청 흐름

WAS `/error-page/500` 다시 요청 -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러(/error-page/500)
-> View

예외 발생과 오류페이지 요청흐름

1. WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)
2. WAS `/error-page/500` 다시 요청 -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러(/error-page/
500) -> View

정리하면 다음과 같다.
  1. 예외가 발생해서 WAS까지 전파된다.
  2. WAS는 설정된 오류페이지 경로를 찾아서, 내부에서 오류페이지를 호출한다.
    이때 오류페이지경로로 필터, 서블릿, 인터셉터, 컨트롤러가 모두 다시 호출된다.

1-4. 서블릿 - 필터 중복 호출 방지


1. WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)
2. WAS `/error-page/500` 다시 요청 -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러(/error-page/
500) -> View

오류가 발생하면 오류페이지를 출력하기 위해, WAS에서 다시 한번 호출이 발생한다고 했다.

이때, 필터 / 서블릿 / 인터셉터도 모두 '다시' 호출된다.

그런데, 로그인 인증체크 같은 경우는, 이미 한번 필터나 인터셉터에서 로그인 체크를 완료했다. 따라서 오류페이지를 서버에서 호출할 때마다 해당 로그인 인증 체크 필터나 인터셉터를 다시 호출하는 일은 상당히 비효율적인 일이다.

서블릿의 필터는, 이런 문제를 해결하기 위해 DispatcherType 이라는 추가 정보를 제공해준다.

DispatcherType으로 클라이언트로부터 발생한 것이 정상 요청인지, 아니면 오류페이지 요청인지를 구분할 수 있게 된다.

DispatcherType

public enum DispatcherType {
 FORWARD, 
 INCLUDE,
 REQUEST, // 클라이언트 요청임
 ASYNC,
 ERROR // 오류 요청임
}

필터와 DispatcherType

실제로 DispatcherType이 어떻게 활용되어서, 오류페이지를 요청할 때 필터를 중복호출하지 않는지를 알아보자.

// LogFilter - DispatcherType 로그 추가
@Slf4j
public class LogFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("!! log filter init !!");
    }

    // 이전에 배웠던 필터 예제에서, getDispathcerType을 추가해준 것만 다름
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;

        String requestURI = httpRequest.getRequestURI();
        String uuid = UUID.randomUUID().toString();

        try {
            log.info("doFilter -- REQUEST [{}][{}][{}] : ", uuid,
                    request.getDispatcherType(),
                    requestURI);

            filterChain.doFilter(request, response);

        } catch (Exception e) {
            throw e;
        } finally {
            log.info("doFilter -- RESPONSE [{}][{}][{}] : ", uuid,
                    request.getDispatcherType(),
                    requestURI);
        }
    }

    @Override
    public void destroy() {
        log.info("!! log filter destroy !!");
    }
}
// WebConfig
@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Bean
    public FilterRegistrationBean logFilter(){
        FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();

        filterFilterRegistrationBean.setFilter(new LogFilter());
        filterFilterRegistrationBean.setOrder(1);
        filterFilterRegistrationBean.addUrlPatterns("/*");

        // 이렇게 두가지를 모두 넣으면, 클라이언트 요청은 물론이고, 오류페이지 요청에서도 필터가 호출된다!!
        /* 아무것도 넣지 않으면, 기본값이 DispatcherType.REQUEST이다. */
        // 즉, 디폴트로는 클라이언트 요청인 경우에만, 필터가 적용된다.

        // 특별히 오류페이지 경로도 필터로 적용할 것이 아니라면, 기본값 그대로 사용해도 됨.
        filterFilterRegistrationBean.setDispatcherTypes
                (DispatcherType.REQUEST
                 /*, DispatcherType.ERROR*/);
            // 만약, 디폴트값으로 한다면, 에러 발생 시, 로그필터에서 REQUEST만 필터링 해주고, 에러는 안해주는 것 확인 가능..
        // 물론, 오류페이지 요청 전용 필터를 적용하고 싶다면, DispatcherType.ERROR도 지정해주면 됨.

        return filterFilterRegistrationBean;
    }
}

그렇다.
필터는 디폴트설정으로 DispatcherType.REQUEST만을 적용해주기에,
오류페이지를 다시 요청할 때 필터가 중복 요청될 것이라는 걸 걱정해주지 않아도 된다.

물론 오류페이지 요청할 때, 오류에도 필터가 적용되길 바란다면 DispatcherType.ERROR를 추가해줘도 되지만, 그럴 일은 별로 없을 것이다.

1-5. 스프링 - 인터셉터 중복 호출 방지

그렇다면, 에러페이지를 다시 요청할 때, 인터셉터 중복 호출은 어떻게 막을 수 있을까?

인터셉터는 신이다.
인터셉터를 적용해주고 싶지 않다면, 아주아주 강력한 excludePathPatterns에 적용해서 빼주면 된다 !!!

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor())
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns(
                        "/css/**", "/*.ico",
                        "/error", "/error-page/**" // 오류페이지 경로
                );
        /* 인터셉터는 신이다. 필터보다 훨씬 간단하군..
        // 그저 인터셉터를 적용하고 싶지 않다면, 그냥 제외 url에 추가하면 된다.. */
    }
}

지금까지의 전체흐름을 정리해보았다.

1. /hello 정상 요청이 들어온다면..
WAS(/hello, dispatchType=REQUEST) -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러 -> View

2. /error-ex로 에러를 발생시켰다면..
- 필터는 디폴트로 DispatcherType으로 중복호출 제거 (dispatchType=REQUEST)
- 인터셉터는 경로정보로 중복호출제거 (excludePathPatterns("/error-page/**")

3. 흐름
1. WAS(/error-ex, dispatchType=REQUEST) -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러
2. WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)
3. WAS 오류 페이지 확인
4. WAS(/error-page/500, dispatchType=ERROR) -> 필터(x) -> 서블릿 -> 인터셉터(x) -> 컨트
롤러(/error-page/500) -> View



2. 스프링 - 오류페이지 만드는 법

지금까지 서블릿을 이용해 오류페이지를 만들려면, 매번 다음과 같은 복잡한 과정을 거쳤다..

  • WebServerCustomizer 에서 ErrorPage 만들기
  • 에러처리용 컨트롤러 ErrorPageController 만들기

하지만, 너무 귀찮다...

스프링부트는, 이런 과정을 모두 기본으로 제공해준다 !!

- ErrorPage 자동 등록
	이때 /error라는 경로로, 기본오류메세지 설정함

- ErrorPageController도 자동 등록
	스프링은 BasicErrorController가 에러페이지 컨트롤러임
   	ErrorPage에 등록한 /error를 매핑해서, 어떤 에러페이지 뷰로 보낼지 처리해주는 컨트롤러

!주의!
스프링부트가 제공하는 기본 오류 메커니즘을 사용할 수 있도록, WebServerCustomizer에 있는 @Component를 주석처리해줘야 한다.

결국 스프링이 다 자동으로 해주니, 개발자가 할일은 오류페이지만 올바른 위치에 만드는 일이다!!

규칙은 다음과 같다.

뷰 선택 우선순위

  BasicErrorController 의 처리 순서
  
  1. 뷰 템플릿
    resources/templates/error/500.html
    resources/templates/error/5xx.html
    
  2. 정적 리소스( static , public )
    resources/static/error/400.html
    resources/static/error/404.html
    resources/static/error/4xx.html
    
  3. 적용 대상이 없을 때 뷰 이름( error )
  	resources/templates/error.html

404, 500처럼 구체적인 것이 5xx처럼 덜 구체적인 것보다 우선순위가 높음.
5xx, 4xx는 각각 500대, 400대 오류를 처리해줌

[test]

http://localhost:8080/error-404  -> 404.html
http://localhost:8080/error-400 -> 4xx.html (400 오류 페이지가 없지만 4xx가 있음)
http://localhost:8080/error-500 -> 500.html
http://localhost:8080/error-ex -> 500.html (예외는 500으로 처리)
profile
배운 내용을 바로바로 기록하자!

0개의 댓글