IJY
느리더라도 꾸준히
IJY
전체 방문자
오늘
어제
  • 분류 전체보기 (67)
    • Develop (67)
      • Java (8)
      • Go (0)
      • Test (1)
      • Web (1)
      • HTML, CSS (1)
      • TIL(Today I Learned) (18)
      • SQL (0)
      • Algorithm (27)
      • 회고 (7)
      • Troubleshooting (1)
      • Etc (3)
    • Etc (0)

블로그 메뉴

  • 홈
  • 태그
  • 방명록
  • 글쓰기

공지사항

인기 글

태그

  • 독후감
  • 알고리즘
  • Interceptor
  • EntityTransaction
  • html
  • Filter
  • REST Assured
  • stream
  • BufferedWriter
  • object
  • Class
  • 초기화
  • PostConstruct
  • Spring
  • recursion
  • 회고
  • instance
  • API 예외 처리
  • 12921
  • web
  • MVC
  • 프로그래머스
  • BufferedReader
  • init
  • sort
  • 소수 찾기
  • java
  • 우테코 온보딩
  • 재귀
  • 백준

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
IJY
Develop/TIL(Today I Learned)

Spring MVC - 예외 처리(Error page)

Spring MVC - 예외 처리(Error page)
Develop/TIL(Today I Learned)

Spring MVC - 예외 처리(Error page)

2023. 2. 20. 15:45

Spring MVC에서 예외 발생시 흐름 및 처리하는 방법에 대해 간단하게 정리를 한 내용입니다.

해당 정리 글에서는 예외 발생 시 전체적인 흐름 및 에러 페이지 처리에 대한 내용을 다루며, API의 예외 처리는 다루지 않습니다.

 

들어가기에 앞서

Spring MVC의 예외 처리에 대한 내용에 들어가기에 앞서 필터와 인터셉터 등에 대한 HTTP 요청의 흐름을 아는 것이 우선이므로 이에 대해 간략히 알아보고 들어가도록 하겠습니다.

 

HTTP 요청 흐름

HTTP 요청에 따른 Servlet Filter & Spring Interceptor 흐름

HTTP 요청에 따른 흐름은 위 이미지와 같이 WAS에서 Servlet Filter를 통과하여 Spring Interceptor를 통과하게 됩니다.

(Filter와 Interceptor에 대한 자세한 내용은 "Servlet Filter & Spring Interceptor"을 참고해주세요.)

 

예외 발생시 흐름

Exception 발생시 흐름

위 HTTP 요청 흐름에서 컨트롤러에서 예외가 발생하게 되면 해당 예외는 위 흐름과 정반대로 흘러가 WAS에 도달하게 되고 예외에 따른 에러 페이지 반환 요청을 하게 됩니다. (Filter나 Interceptor에서 예외에 대한 처리를 했다면 WAS까지 도달하지 못하겠지만요)

 

예외 처리 트리거

위와 같은 흐름의 예외 처리를 발생시키는 트리거는 2가지가 존재한다.

  1. Exception(예외) 발생
  2. response.sendError(HTTP 상태 코드, 에러 메시지) 메서드 호출

1번의 경우 진짜로 예외가 발생하여 WAS에 도달한 경우를 의미하고, 2번의 경우 개발자가 직접 메서드를 호출하여 에러가 발생했다고 전달하는 것을 의미한다. 이 두 방법 모두 처리되는 동작은 위 흐름과 동일하다.

 

예외 처리 방법

예외 발생 후 WAS까지 예외가 도달했다면 WAS는 해당 예외와 매핑되는 에러 페이지가 있는지 조회를 한 후 이에 해당하는 컨트롤러에 요청을 보내게 된다. 매핑되는 에러 페이지가 없다면 기본 에러 페이지에 HTTP status code 500(Internal server error)으로 처리한다.

즉, 에러 페이지를 구현 후(정적 HTML 또는 뷰 템플릿) 해당 페이지를 반환하는 컨트롤러를 구현하고 이를 예외와 매핑시키면 된다.

  1. 에러 페이지 구현 (정적 HTML 또는 뷰 템플릿)
  2. 에러 페이지를 호출하는 컨트롤러 구현
  3. 예외와 컨트롤러 매핑

구현 예제 코드

더보기

에러 페이지 구현 - 스킵

정적 HTML 페이지인 경우 : /resources/static/error-page/404.html, /resources/static/error-page/500.html

뷰 템플릿을 사용하는 경우 : /resources/templates/error-page/404.html, /resources/templates/error-page/500.html

(경로는 편한대로 설정 가능, 해당 예제에서는 위 경로 사용)

 

에러 페이지 호출 컨트롤러 코드

import javax.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class ErrorPageController {

    @RequestMapping("/error-page/404")
    public String errorPage404(HttpServletRequest request) {
        // 만약 뷰 템플릿을 이용하는 경우
        // request.setAttribute(Name, Value); 사용하여 전달
        return "error-page/404";
    }

    @RequestMapping("/error-page/500")
    public String errorPage500(HttpServletRequest request) {
        // 만약 뷰 템플릿을 이용하는 경우
        // request.setAttribute(Name, Value); 사용하여 전달
        return "error-page/500";
    }

}

에러 페이지 등록 코드

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;

@Component
public class WebServerCustomizer implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {

    @Override
    public void customize(ConfigurableWebServerFactory factory) {
        // HTTP Status 404 (Not found) 예외 발생시 "/error-page/404"를 호출하도록 매핑
        ErrorPage error404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-page/404");
        
        // HTTP Status 500 (Internal Server Error) 예외 발생시 "/error-page/500"를 호출하도록 매핑
        ErrorPage error500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error-page/500");
        
        // RuntimeException 발생시 "/error-page/501"를 호출하도록 매핑 => "/error-page/501"은 존재하지 않으므로 404 에러 페이지 발생
        ErrorPage errorRunEx = new ErrorPage(RuntimeException.class, "/error-page/501");
        
        // 주의사항 : Exception.class로 매핑하게되면 상세 예외를 매핑하더라도 Eception.class와 매핑된 페이지 우선 호출
        // 따라서 아래의 errorEx를 addErrorPages() 메서드 파라미터에 추가하면 RuntimeException이 발생하더라도 "/error-page/500"을 호출
        ErrorPage errorEx = new ErrorPage(Exception.class, "/error-page/500");
        
        // 테스트 결과 Exception만 이러한 현상이 발생
        // RuntimeException을 상속받는 다른 예외로 테스트를 한 결과 정확히 일치하는 예외와 매핑된 페이지 우선 호출
        // 만약 등록된 예외가 아니라면 등록된 상위 예외를 찾아서 매핑된 페이지 호출
        // Exception에 대한 동작만 왜이러지..?

        // 위에서 매핑시킨 내용을 등록
        factory.addErrorPages(error404, error500, errorRunEx);
    }
}

 

Spring boot 사용 시 간단한 처리 방법

사실 Spring boot를 사용하는 프로젝트라면 위와 같은 방법 말고 간단한 방법이 존재한다. (동작 방식은 동일)

따라서 간단하게 기본 경로인 "/error"에 에러 페이지만 만들어주면 동작을 하며, 에러 페이지의 이름은 다음과 같이 /error/404.html, /error/500.html, /error/4xx.html 등의 파일명으로 만들어주면 된다.

이때 에러 페이지의 매핑 우선순위는 다음과 같다.

  1. 뷰 템플릿
    1. /resources/templates/error 에 위치한 구체적인 파일 (ex : 404.html)
    2. /resources/templates/error 에 위치한 덜 구체적인 파일 (ex : 4xx.html)
  2. 정적 리소스
    /resources/static/error 에 위치한 구체적인 파일 (ex : 404.html)
    /resources/static/error 에 위치한 덜 구체적인 파일 (ex : 4xx.html)
  3. 기본 페이지
    /resources/templates/error.html

이러한 동작이 가능한 이유는 Spring boot에서 기본적으로 다음과 같은 컨트롤러와 설정을 제공하기 때문이다.

  • BasicErrorController : 에러 페이지 호출 컨트롤러
  • ErrorMvcAutoConfiguration : 에러 페이지 등록 클래스

즉, Spring boot에서 위에서 사용한 방법대로 구현하여 제공하고 있는 것

 

결론

Spring boot를 사용하는 프로젝트면서 예외 발생 시 에러 페이지를 반환하는 동작을 한다면,

에러 페이지 HTML을 만들어 추가만 해주면 동작을 하게 된다.

 

예외 발생시 전체적인 흐름

위 이미지에서 preHandle, postHandle, afterCompletion 이렇게 3개의 메서드가 Spring Interceptor에서 제공하는 기능


아래의 내용은 예외 처리에 대한 내용이 아닌 예외 처리 동작에 따른 Filter 및 Interceptor에 대한 추가적인 내용입니다.

 

WAS의 에러 페이지 호출

예외 처리에 대한 내용을 알아보았는데 여기서 흐름 부분을 보다 보면 의문이 생길 수 있다.

HTTP 요청이 필터와 인터셉터를 통과해 컨트롤러까지 갔다가 예외가 발생해 WAS로 돌아온 후 WAS가 예외 페이지를 호출하기 위해 컨트롤러로 요청을 보내는데, 이때 필터와 인터셉터를 다시 거치게 되는 것이다. 이미 한번 통과를 했던 필터와 인터셉터를 에러 페이지 요청에서 다시 통과를 해야 하는 것은 비효율적인 동작이 아닐까? (특정 필터나 인터셉터는 에러 페이지 호출에 대해서도 필요할 수 있지만..)

이미 이러한 경우에 대해서도 처리가 가능하도록 기능이 제공되고 있으며, 이는 필터 및 인터셉터의 등록 과정에서 처리하도록 되어있다.

자세한 등록 과정은 이전에 작성한 글(위 HTTP 요청 흐름에 링크를 걸어둔 글)에 정리를 해 두었으므로 생략하고, 여기선 간단하게 정리를 하겠다.

 

Filter

Filter의 경우 이러한 문제를 해결하기 위한 기능으로 DispatcherType이라는 옵션을 제공하고 있으며, 아래와 같은 상태를 갖고 있다.

  • FORWARD : Servlet에서 다른 서블릿 또는 JSP를 호출하는 경우
    RequestDispatcher.forward(ServletRequest, ServletResponse) 메서드
  • INCLUDE : Servlet에서 다른 서블릿 또는 JSP의 결과를 포함하여 사용하려는 경우
    RequestDispatcher.include(ServletRequest, ServletResponse) 메서드
  • REQUEST : 클라이언트의 요청(HTTP Request)인 경우
  • ASYNC : 비동기 호출인 경우
    AsyncContext.dispatch() 메서드
    AsyncContext.dispatch(String) 메서드
    AsyncContext.addListener(AsyncListener, ServletRequest, ServletResponse) 메서드
  • ERROR : 오류 처리기 메커니즘으로 처리를 전달한 경우 (에러 페이지 요청 등)

지금 여기서 봐야하는 상태는 REQUEST와 ERROR이다.

Filter 적용 시점에서 해당 요청의 DispatcherType이 REQUEST인지 ERROR인지 또는 둘 다인지에 따라 적용 여부를 결정할 수 있다.

해당 방법은 "FilterRegistrationBean" 클래스의 "setDispatcherType(DispatcherType...)" 메서드를 통해서 지정할 수 있다.

예제 코드

더보기
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;

@Configuration
public class WebConfig {
    @Bean
    public FilterRegistrationBean httpLogFilter() {
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
        
        // 위에서 구현한 로그 필터를 적용
        filterRegistrationBean.setFilter(new HttpLogFilter());
        
        // 해당 필터의 동작 순서를 설정 (필터 체인에서 몇 번째로 동작할 필터인지 지정)
        filterRegistrationBean.setOrder(1);
        
        // 해당 필터를 적용 할 요청에 대해 설정, 여기선 모든 요청에 적용
        filterRegistrationBean.setUrlPatterns("/*");
        
        // 해당 필터를 어떤 요청(DispatcherType)에 적용할지 설정
        // 여기선 일반 요청과 에러 요청 둘 다 해당 필터 적용
        // 참고로 해당 설정을 생략하면 REQUEST에만 적용
        filterRegistrationBean.setDispatcherType(DispatcherType.REQEUST, DispatcherType.ERROR);
        
        return filterRegistrationBean;
    }

 

Interceptor

Interceptor의 경우 DispatcherType을 사용하지 않고 간단하게 "excludePathPatterns(String)" 메서드를 사용하여 해당 요청을 제외시키면 끝이다.

예제 코드

더보기
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    
        registry.addInterceptor(new HttpLogInterceptor)
            .order(1)
            .addPathPatterns("/**")
            .excludePathPatterns("/*.ico", "/css/**", "/error-page/**");
            // 해당 메서드에 포함되는 경로의 요청에 대해선 해당 인터셉터 미적용
    }
}

'Develop > TIL(Today I Learned)' 카테고리의 다른 글

Spring Converter & Formatter  (0) 2023.02.24
Spring MVC - 예외 처리(API)  (0) 2023.02.23
Servlet Filter & Spring Interceptor  (0) 2023.02.20
JPA - 엔티티 수정(merge, dirty check)에 대한 내용 정리  (0) 2023.02.15
트랜잭션 스크립트 패턴과 도메인 모델 패턴  (0) 2023.02.14
  • 들어가기에 앞서
  • HTTP 요청 흐름
  • 예외 발생시 흐름
  • 예외 처리 트리거
  • 예외 처리 방법
  • Spring boot 사용 시 간단한 처리 방법
  • 결론
  • WAS의 에러 페이지 호출
'Develop/TIL(Today I Learned)' 카테고리의 다른 글
  • Spring Converter & Formatter
  • Spring MVC - 예외 처리(API)
  • Servlet Filter & Spring Interceptor
  • JPA - 엔티티 수정(merge, dirty check)에 대한 내용 정리
IJY
IJY
개발 관련 공부한 내용을 정리하는 블로그입니다. 느리더라도 꾸준히 포스팅을 하려고 노력합니다.

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.