예외처리로 화면뿌리기

허정현·2024년 10월 9일

Spring boot

목록 보기
2/4

📌 글을 작성하는 이유와 목표

  • 스프링 부트가 제공하는 예외 페이지 기능을 이해하기 위함.
  • 서블릿의 불편한 방식부터 스프링 부트의 편리한 방식까지의 과정
  • 컨트롤러 -> 인터셉터 -> 서블릿 -> 필터 -> WAS 까지의 예외 전달 과정을 이해하기 위함.

📌 예외 전달 과정

  1. 컨트롤러에서 요청을 처리하는 도중 예외가 발생하여 예외가 상위 계층으로 올라간다.
  2. HandlerInterCeptor를 통해 postHandle, afterCompletion 메서드를 통해 예외를 잡아낼 수 있지만, 무시하면 서블릿까지 전파된다.
  3. 서블릿까지 예외가 도달할 경우 DisPatcherServlet에서 예외를 처리할 수 있는 핸들러를 찾는다. 여기서도 예외가 처리되지 않는다면 필터로 전파된다.
  4. 요청, 응답을 가로채서 예외에 대한 추가적인 처리가 가능하기 때문에, 로그, 특정 예외 처리 로직도 실행가능함. 하지만, 이 글은 WAS까지 가는 과정을 얘기중이다.
  5. 최종적으로 Web Application Sever까지 도달하게 되고, 처리하지 못한 예외를 클라이언트에게 반환해줘야하는 책임이 있다. 클라이언트에서 요청한 응답을 찾지 못한 것이고, 오류가 생겼기 때문에 상태 코드를 클라이언트에 응답한다. 여기서 WebServerCustomizer를 통해 오류 페이지를 등록하고, 해당 오류 페이지 정보에 대한 내용을 우리가 직접 저장하고, 해당 오류에 대한 페이지 경로를 지정해주면 WAS는 해당 오류 페이지를 찾아 제공해준다.
    또한 이 과정에서 다시 컨트롤러까지 호출하기 때문에
    request로 오류 정보를 추가해서 넘겨 준다.

📌 서블릿의 기본 예외처리 방식

  • HttpServletResponse가 제공하는 기능을 사용해 오류 상태 코드와 메시지를 전달하는 response.sendError(HTTP 상태 코드, 오류 메시지)

  • 자바 자체의 exception

    public void errorEx(HttpServletResponse response) throws IOException {
        throw new RuntimeException();
    }
    @GetMapping("/error-404")
    public void error404(HttpServletResponse response) throws IOException {
        response.sendError(404, "404 오류 ");
    }

위의 그림과 같이 아주 기본적인 화면이 제공된다.

위의 기본적인 화면을 사용자들에게 보여주기 거부감들기 때문에, 우리는 좀 더 이쁜 페이지를 오류화면으로 제공해줄 수 있다.

📌 서블릿의 오류 화면 제공

오류 페이지 정보 등록 클래스가 필요.

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

위의 코드에 처리하고 싶은 오류 페이지 정보를 등록해야함.
ErrorPage의 생성자를 보면, 상태와 경로를 보내는데
여기에 HttpStatus.NOT_FOUND와 같은 404에러 코드를 입력하고, 경로를 추가해주면 이 경로를 통해 사용자에게 오류 화면을 제공해준다.

public ErrorPage(HttpStatus status, String path) {
        this.status = status;
        this.exception = null;
        this.path = 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.addErrorPage(errorPage404,errorPage500,errorPageEx);

위의 경로를 처리해야 우리가 직접 지정한 오류 화면을 제공할 수 있으므로, 해당 컨트롤러를 작성해줘야한다.

@Slf4j
 @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";
     }
그리고 화면에 띄울 HTML파일도 작성해준다. 
<div class="container" style="max-width: 600px">
     <div class="py-5 text-center">
<h2>500 오류 화면</h2> </div>
<div>
<p>오류 화면 입니다.</p>
</div>
     <hr class="my-4">
 </div> <!-- /container -->
 </body>
 </html>

이제 위의 처리가 끝났으면, WAS까지 예외가 전파되고 WAS는 해당 예외가 오류 페이지 정보에 등록되어있다면 해당 예외의 경로를 찾아 다시 컨트롤러를 호출한다. 그 후 컨트롤러에서 해당 오류 페이지 HTML을 반환하여 오류 페이지를 클라이언트에게 제공해준다.

  • 추가로 해당 컨트롤러까지 다시 호출되는 과정에서 WAS는 request에 오류 정보를 담아준다.

request.attribute에 서버가 담아준 정보 javax.servlet.error.exception : 예외 javax.servlet.error.exception_type : 예외 타입 javax.servlet.error.message : 오류 메시지 javax.servlet.error.request_uri : 클라이언트 요청 URI javax.servlet.error.servlet_name : 오류가 발생한 서블릿 이름 javax.servlet.error.status_code : HTTP 상태 코드

📌 서블릿 예외 처리 필터 📌서블릿 예외 처리 인터셉터

위의 글을 읽어보면 다음과 같이 예외처리가 된다.
1. 컨트롤러 -> WAS
2. WAS(페이지경로 전달) -> 컨트롤러

문제점

  • 고객이 요청한건지, 서버가 내부에서 요청한건지 알 수 없음.
  • 로그인과 인증 체크 과정에서 이미 필터나 인터셉터를 통한 인증 체크가 완료되어 있음. 하지만, 오류 페이지 호출 때문에 다시 필터와, 인터셉터를 호출하는 것은 비효율적임.
  • 따라서, 고객이 요청한건지, 서버가 내부에서 오류 페이지를 요청하는 건지 구분해야한다.

Fiter가 제공하는 dispatcherTypes

dispatcherTypes이라는 enum을 이용하여 구분하는데,

public enum DispatcherType {
     FORWARD,
     INCLUDE,
     REQUEST,
     ASYNC,
     ERROR
}

다음과 같은 키워드가 있지만, 오류와 클라이언트의 요청만 구분해보면 두 가지가 있다.
REQUEST : 클라이언트의 요청
ERROR : 오류

이 DispatcherTypes와 Filter를 이용하면 클라이언트의 요청과 오류를 구분할 수 있다.


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

위의 코드를 보면 HttpServletRequest로 request를 캐스팅한 후 요청 URI를 확인하는 코드가 있다. 그 후 UUID를 생성하여 식별자를 구분해주고, try메서드가 실행되면 요청을 가로채어 해당 로그를 남기도록 되어 있다.


결과

REQUEST [a13131eb-07e7-4254-97eb-d018a6380c02][[}][REQUEST]
RESPONSE [a13131eb-07e7-4254-97eb-d018a6380c02][REQUEST][/error-page/500]

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

생각보다 간단하다.
우선, 핸들러 인터셉터의 메서드를 생각해보면
Pre, Post, After 인터셉터 메서드가 있다. 무엇이 차이인지는 알 것이라 생각하고 중요한 점은 InterCeptor에서도 로그를 생성한다는 것이다. 근데 클라이언트의 요청과 오류를 구분하려는 것이기 때문에 DispatcherTypes를 이용하고 싶지만, 스프링의 기능이기에 사용이 불가능하다.
다만, WebConfig 클래스를 만들어 처음부터 인터셉터 경로를 설정하기 때문에, 이러한 설정을 사용해 서 오류 페이지 경로를 excludePathPatterns 를 사용해서 빼주면 된다.

Override
     public void addInterceptors(InterceptorRegistry registry) {
         registry.addInterceptor(new LogInterceptor())
                 .order(1)
                 .addPathPatterns("/**")
                 .excludePathPatterns(
"/css/**", "/*.ico", "/error", "/error-page/**" //오류 페이지 경로
); }

대신 중복 호출을 고려해, Filter와 InterCeptor중 어느 것을 호출할지 고민해보자.

📌 스프링 부트 오류 페이지

기존의 처리 방식

  • WebServerCustomizerErrorPage 추가
  • 예외 처리용 컨트롤러 생성

Spring Boot의 처리 방식

  • ErrorPage를 자동으로 등록하고, /error 경로로 기본 오류 페이지를 지정해줌.
  • BasicErrorController라는 스프링 컨트롤러를 자동으로 등록해주는데, ErrorPage에서 등록한 /error를 매핑해서 처리하는 컨트롤러다.

ErrorMvcAutoConfiguration 클래스가 오류 페이지를 자동으로 등록한다.

  • 기존의 처리 방식의 WebServerCustomizer의 component를 없애고, ErrorPageController또한 주석처리하였다.
  • 경로에 없는 Url를 검색하면 다음과 같이 자동으로 기본 화면이 나온다.

BasicErrorController에서 templates/error의 경로에 들어가 자동으로 관련 에러 html을 찾아 화면에 뿌려주기 때문.

  • 400.html과 4xx.html 파일이 있을 경우 구체적인 400에러이면 400.html이 뿌려지게 된다. 만약, 400.html파일이 없다면 4xx.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`)
  • 서버에서 띄워서 확인하는게 좋음
pplication.properties`
`server.error.include-exception=false` : `exception` 포함 여부( `true` , `false` )
`server.error.include-message=never` : `message` 포함 여부
`server.error.include-stacktrace=never` : `trace` 포함 여부 
`server.error.include-binding-errors=never` : `errors ` 포함 여부

기본 값이 never 인 부분은 다음 3가지 옵션을 사용할 수 있다. never, always, on_param
never : 사용하지 않음
always :항상 사용
on_param : 파라미터가 있을 때 사용

스프링 부트 오류 관련 옵션
server.error.whitelabel.enabled=true : 오류 처리 화면을 못 찾을 시, 스프링 whitelabel 오류 페이지 적용
server.error.path=/error : 오류 페이지 경로, 스프링이 자동 등록하는 서블릿 글로벌 오류 페이지 경로
BasicErrorController 오류 컨트롤러 경로에 함께 사용된다.


그 외 부분을 좀 적용시켜보도록.

profile
지식 정리를 잘 할 수 있을 때까지

0개의 댓글