본문 바로가기
Spring Study/MVC 패턴

[MVC 패턴] 예외 처리와 오류 페이지

by 정재인 2023. 9. 1.

서블릿 예외 처리

1. Exception(예외)
2. response.sendError(HTTP 상태코드, 오류 메시지)

 

Exception(예외)

1. 자바 직접 실행

자바의 메인 메서드를 직접 실행하는 경우 main이라는 이름의 쓰레드가 실행된다. 실행 도중 예외를 잡지 못하고 처음 실행한 main 메서드를 넘어서 예외가 던져지면, 예외 정보를 남기고 해당 쓰레드는 종료된다.

 

2. 웹 어플리케이션

웹 어플리케이션은 사용자 요청별로 별로의 쓰레드가 할당되고, 서블릿 컨테이너 안에서 실행된다.

컨트롤러(예외 발생) → 인터셉터 → 서블릿 → 필터 → WAS
package hello.exception.servlet;

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

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

 

response.sendError(HTTP 상태코드, 오류 메시지)

오류가 발생했을 때 HttpServletResponse가 제공하는 sendError라는 메서드를 사용해도 된다.

이것을 호출한다고 당장 예외가 발생하는 것은 아니지만, 서블릿 컨테이너에게 오류가 발생했다는 점을 전달할 수 있다.

response.sendError(HTTP 상태코드)
response.sendError(HTTP 상태코드, 오류 메시지)
@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()를 호출하면 response 내부에 오류가 발생했다는 상태를 저장해둔다. 그리고 서블릿 컨테이너는 고객에게 응답 전 response에 sendError()가 호출되었는지 확인한다. 호출되었다면 설정한 오류 코드에 맞추어 기본 오류 페이지를 보여준다.

 


오류 페이지 작동 원리

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

 

예외 발생 흐름

컨트롤러(예외 발생) → 인터셉터 → 서블릿 → 필터 → WAS

 

sendError 흐름

컨트롤러(response.sendError()) → 인터셉터 → 서블릿 → 필터 → WAS

 

오류 페이지 요청 흐름 ex)error-page 500 발생

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

 

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

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

중요한 점은 웹 브라우저(클라이언트)는 서버 내부에서 이런 일이 일어나는지 전혀 모른다는 이다. 오직 서버 내부에서 오류 페이지를 찾기 위해 추가적인 호출을 한다.

 


서블릿 예외 처리 - 필터

오류가 발생하면 오류 페이지를 출력하기 위해 WAS 내부에서 다시 한번 호출이 발생한다. 이때 필터, 서블릿, 인터셉터도 모두 다시 호출된다. 하지만 이러한 점은 매우 비효율적이다.

이러한 비효율적인 부분을 개선하기 위해 제공된 것이 DispatcherType이다.

 

DispatcherType

package hello.exception.filter;

import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.UUID;

@Slf4j
public class LogFilter 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 {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String requestURI = httpRequest.getRequestURI();

        String uuid = UUID.randomUUID().toString();
        try {
            log.info("REQUEST  [{}][{}][{}]", uuid, request.getDispatcherType(), requestURI);
            chain.doFilter(request, response);
          } catch (Exception e) {
              throw e;
          } finally {
              log.info("RESPONSE [{}][{}][{}]", uuid, request.getDispatcherType(), requestURI);
          }
}
      @Override
      public void destroy() {
          log.info("log filter destroy");
      }
}

 

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

package hello.exception;
  import hello.exception.filter.LogFilter;
  import hello.exception.interceptor.LogInterceptor;
  import org.springframework.boot.web.servlet.FilterRegistrationBean;
  import org.springframework.context.annotation.Configuration;
  import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
  import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
  import javax.servlet.DispatcherType;
  import javax.servlet.Filter;
  @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/**" //오류 페이지 경로
);
    //@Bean
    public FilterRegistrationBean logFilter() {
        FilterRegistrationBean<Filter> filterRegistrationBean = new
FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
          filterRegistrationBean.setOrder(1);
          filterRegistrationBean.addUrlPatterns("/*");
          filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST,
  DispatcherType.ERROR);
          return filterRegistrationBean;
} }

 


스프링 부트 - 오류 페이지

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

ErrorPage를 자동으로 등록한다. 이때 /error라는 경로로 기본 오류 페이지를 설정한다.

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

- 서블릿 밖으로 예외가 발생하거나, response.sendError(. . .)가 호출되면 모든 오류는 /error를 호출하게 된다.

BasicErrorController라는 스프링 컨트롤러를 자동으로 등록한다.

- ErrorPage에서 등록한 /error를 매핑해서 처리하는 컨트롤러다.

오류가 발생했을 때 오류 페이지로 /error를 기본 요청한다. 스프링 부트가 자동 등록한 BasicErrorController는 이 경로를 기본으로 받는다.

따라서 개발자는 오류 페이지만 등록하면 된다.

BasicErrorController는 기본적인 로직이 모두 개발되어 있다. 따라서 개발자는 오류 페이지 화면만 BasicErrorController가 제공하는 룰과 우선순위에 따라서 등록하면 된다. 정적 HTML이면 정적 리소스, 뷰 템플릿을 사용하고 동적으로 오류 화면을 만들고 싶으면 뷰 템플릿 경로에 오류 페이지 파일을 만들어서 넣어두면 된다.

 

BasicErrorController는 다음 정보를 model에 담에 뷰에 전달한다.

* 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`)
<!DOCTYPE HTML>
  <html xmlns:th="http://www.thymeleaf.org">
  <head>
      <meta charset="utf-8">
  </head>
<body>
  <div class="container" style="max-width: 600px">
      <div class="py-5 text-center">
<h2>500 오류 화면 스프링 부트 제공</h2> </div>
<div>
<p>오류 화면 입니다.</p>
</div> <ul>
<li>오류 정보</li> <ul>
              <li th:text="|timestamp: ${timestamp}|"></li>
              <li th:text="|path: ${path}|"></li>
              <li th:text="|status: ${status}|"></li>
              <li th:text="|message: ${message}|"></li>
              <li th:text="|error: ${error}|"></li>
              <li th:text="|exception: ${exception}|"></li>
              <li th:text="|errors: ${errors}|"></li>
              <li th:text="|trace: ${trace}|"></li>
</ul>
</li> </ul>
      <hr class="my-4">
  </div> <!-- /container -->
   </body>
  </html>

댓글