스프링 mvc - 예외처리 / 오류 페이지

meluu_·2024년 1월 24일
0

스프링

목록 보기
24/27
post-thumbnail

🌿 시작하기 앞서


스프링 부트 3.2.1 버전을 기준으로 작성됨


🌱 서블릿 예외 처리


종류

  • Exception (예외)
  • response.sendError(Http 상태 코드, 오류 메시지)

✔️ Exception(예외)


자바 직접 실행

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

웹 애플리케이션

  • 사용자 요청별로 별도의 쓰레드가 할당, 서블릿 컨테이너 안에서 실행
  • 애플리케이션에서 예외 발생, 어디선가 try~catch로 예외를 잡아서 처리하면 문제 없음
  • 잡지 못하고 서블릿 밖으로 예외가 던져지면 ?

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

Exception 의 경우 서버 내부에서 처리할 수 없는 오류가 발생한 것으로 생각해서 HTTP 상태 코드 500을 반환


✔️ response.sendError()


  • 오류 발생시 사용
  • HttpServletResponse가 제공한다.
  • 서블릿 컨테이너에게 오류가 발생했다는 점을 전달
  • Http 상태 코드와 오류 메시지 추가 가능

흐름

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

  • response 내부에는 오류가 발생했다는 상태를 저장
  • 응답 전에 response 에 sendError() 가 호출되었는지 확인
  • 설정한 오류 코드에 맞추어 기본 오류 페이지 보여줌
// 서블릿 컨테이너 기본 제공 오류화면
http://localhost:8080/error-ex
http://localhost:8080/error-404
http://localhost:8080/error-500

✔️ 오류 화면 제공


서블릿 기본 제공 예외 처리 화면은 고객친화적이지 않음
스프링 부트가 제공하는 기능을 통해 서블릿 오류 페이지 등록

@Component
public class WebServerCustomizer implements
        WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
    @Override
    public void customize(ConfigurableWebServerFactory factory) {
        ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/errorpage/404");
        ErrorPage errorPage500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error-page/500");
        //RuntimeException 또는 그 자식 타입의 예외
        ErrorPage errorPageEx = new ErrorPage(RuntimeException.class, "/error-page/500");
                factory.addErrorPages(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";
    }
}

view는 컨트롤러에서 지정한 경로에 원하는대로 만들어주면 된다.

오류 페이지 작동 원리

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

중요한점
웹 브라우저(클라이언트)는 서버 내부의 이런과정을 모른다.
오직 서버 내부에서 오류 페이지를 찾기 위해 내부적으로 재호출한다.

오류정보 추가
request.attribute에 서버가 담아준 정보

// RequestDispatcher 상수로 정의됨
ERROR_EXCEPTION = "jakarta.servlet.error.exception"; 예외
ERROR_EXCEPTION_TYPE = "jakarta.servlet.error.exception_type"; 예외 타입
ERROR_MESSAGE = "jakarta.servlet.error.message"; 오류 메시지
ERROR_REQUEST_URI = "jakarta.servlet.error.request_uri"; 클라이언트 요청 URI
ERROR_SERVLET_NAME = "jakarta.servlet.error.servlet_name"; 오류가 발생한 서블릿 이름
ERROR_STATUS_CODE = "jakarta.servlet.error.status_code"; HTTP 상태 코드


// 사용
request.getAttribute(ERROR_STATUS_CODE)
request.getDispatcherType() // REQUEST,ERROR 등 어떤 요청인지 정보가 출력됨 

✔️ 필터


예외 발생 및 오류 페이지 요청 흐름에서
필터나 인터셉트가 한 번 더 호출되어 검증하는 것은 비효율적이다.

DispatcherType으로 REQUEST고객 요청이면 진행하고
Error 내부 호출이면 필터와 인터셉트를 건너뛰게 한다.

DispatcherType
REQUEST : 클라이언트 요청
ERROR : 오류 요청
FORWARD : MVC에서 배웠던 서블릿에서 다른 서블릿이나 JSP를 호출할 때
RequestDispatcher.forward(request, response);
INCLUDE : 서블릿에서 다른 서블릿이나 JSP의 결과를 포함할 때
RequestDispatcher.include(request, response);
ASYNC : 서블릿 비동기 호출
Logfilter
@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 {
        // ...
        try {
            log.info("REQUEST [{}][{}][{}]", uuid, request.getDispatcherType(), requestURI);
            chain.doFilter(request, response);
            
        } catch (Exception e) {
            log.info("EXCEPTION {}", e.getMessage());
            throw e;
            
        } finally {
            log.info("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> filterRegistrationBean = new
                FilterRegistrationBean<>();
        filterRegistrationBean.setFilter(new LogFilter());
        filterRegistrationBean.setOrder(1);
        filterRegistrationBean.addUrlPatterns("/*");
        filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
        return filterRegistrationBean;
    }
}

filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
기본 값 : REQUEST

사실 기본 값이 request만 필터 적용되기에 상관없다
단지 다른 type에도 적용하고싶다면 넣어주면된다.


✔️ 인터셉터


인터셉터 중복 호출 제거
인터셉터는 스프링 제공 기능이기에 DispatcherType과 상관없음

오류 페이지 경로를 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/**"); // 오류페이지 경로
    }
    
    // 필터
}

전체 흐름


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

=============================================================================
/error-ex 오류 요청
필터는 DispatchType 으로 중복 호출 제거 ( dispatchType=REQUEST )
인터셉터는 경로 정보로 중복 호출 제거( excludePathPatterns("/error-page/**") )


🌿 스프링 부트 - 오류페이지


스프링 부트는

  • ErrorPage 자동 등록 : /error 경로로 기본 오류 페이지를 설정
    • new ErrorPage("/error"), 상태코드와 예외를 설정하지 않으면 기본 오류 페이지로 사용됨
    • 서블릿 밖 예외 발생, response.sendError()가 호출되면 모든 오류는 /error를 호출
  • BasicErrorController라는 스프링 컨트롤러를 자동으로 등록
    • ErrorPage에서 등록한 /error를 매핑해서 처리

주의
WebServerCustomizer에 있는 @Component 를 주석처리할 것



BasicErrorController는 기본 로직이 모두 개발된 상태

view 파일만 넣어두면 된다.

// 뷰 선택 우선순위
// 5xx, 4xx 라고 하면 500대, 400대 오류를 처리

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

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`)

오류 정보 model 포함 여부
application.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 : 사용하지 않음
always :항상 사용
on_param : 파라미터가 있을 때 사용 
// (message=&errors=&trace=) 이런식으로 파라미터를 전달하면 model에 담김

❗ 주의
실무에서 오류 정보 사용자에게 노출 금지


부록


server.error.whitelabel.enabled=true : 오류 처리 화면을 못 찾을 시, 스프링 whitelabel 오류 페이지 적용
server.error.path=/error : 오류 페이지 경로, 스프링이 자동 등록하는 서블릿 글로벌 오류 페이지 경로

확장 포인트
에러 공통 처리 컨트롤러의 기능 변경을 원한다면
ErrorController or BasicErrorController 상속받아서 기능 추가

하지만 대부분 기본 기능으로 손쉽게 처리 가능



🔖 학습내용 출처

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술

profile
열심히 살자

0개의 댓글