🌿 스프링/스프링 MVC 2편

서블릿 예외 처리와 오류 페이지

le2donguk 2026. 2. 15. 01:50

개요

서블릿은 다음 2가지 방식으로 예외를 처리한다

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

 

Exception

 

오류 처리 방식

자바를 실행시키면 main 이라는 이름의 스레드가 실행된다. 실행 도중에 어떠 한 부분에서 예외를 못 잡으면 계속 상위 부모에게 던지는데 그렇게 계속 던지다가 main 밖으로 까지 던져지면 예외 정보를 남기고 해당 스레드를 종료한다

 

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

 

 

Exception이 발생하고 WAS 까지 전달되면 WAS는 서버 내부에서 처리할 수 없는 오류가 발생한 것으로 생각해서 HTTP 상태코드 500을 반환한다

명심하고 또 명심할 건 예외를 발생 되면 무조건 상태코드는 500을 반환하는 것!

 

 

 

 

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

오류가 발생했을 때 예외를 발생시키는 게 아니라 response에 sendError 메서드를 통해서 WAS에게 알려 줄 수 있다

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

 

 

sendError 흐름

WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(response.sendError)

 

 

sendError를 하면 좋은 점이  상태 코드가 500 에러가 아닌 우리가 직접 ErrorCode를 입력하고 , 오류 메시지를 작성할 수 있다

 

 

 

정리하자면

  • 예외가 터지면 WAS의 ServeltContainer는 500으로 처리한다
  • response.sendError → 직접 ErrorCode, 메시지 입력 가능

 


 

서블릿 예외 처리에서 오류 화면 제공해 보자

해당 기능을 하기 위해선 Web 가장 기본적인 방법은 각각의 오류를 직접 errorPage로 Mapping 하는 것이다

그렇게 하기 위해선 다음과 같은 Bean 이 필요하다

 

@Component
public class WebServerCustomizer implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {

    @Override
    public void customize(ConfigurableWebServerFactory factory) {
        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.addErrorPages(errorPage404,errorPage500,errorPageEx);

    }
}

 

ErrorPage 객체를 통해서 

특정 상태코드는 특정 오류페이지로 가세요 라는 정보를 가진 객체를 만들고 해당 객체를 등록을 하면 된다 

 

어떻게 오류 페이지로 보낼 수 있는 걸까? 

 

전체 흐름

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

 

 

response.sendError를 통해서 WAS까지 에러가 전달되면 톰캣은 response을 열어봐서 Error 가 있는지 확인한다

만약 Error 가 있으면 어떤 페이지로 가야 할지 확인하고 해당 페이지로 요청을 다시 보내준다

 

근데.. 문제가 WAS까지 Error가 올라가서 다시 뷰템플릿으로 갈 때 

필터... 인터셉터... 다시 또 거치게 된다 

 

이미 한번 필터나 인터셉터에서 로그인 인증 체크를 완료했는데 이렇게 가는데 그러면 이렇게 되면 필터, 인터셉터를 2번 탄다 너무 비효율적이다

그래서 이게 클라이언트로부터 발생한 요청인지 아니면 오류페이지를 출력하기 위한 내부 요청인지 구분할 수 있어야 한다

이런 문제를 해결하기 위해서 WAS 가 에러페이지로 요청을 보낼 때  request 객체의 DispatcherType을 이용해 Filter를 다시 구성해야 한다

 

 

DispatcherType

WAS 가 오류페이지를 호출하기 위해 다시 컨트롤러를 호출할 때 reqeust 랑 response에 Attribute에 Error 정보를 준다는 건 알고 있다

log.info("ERROR_EXCEPTION: {}",request.getAttribute(ERROR_EXCEPTION));
log.info("ERROR_EXCEPTION_TYPE: {}",request.getAttribute(ERROR_EXCEPTION_TYPE));
log.info("ERROR_MESSAGE: {}",request.getAttribute(ERROR_MESSAGE));
log.info("ERROR_REQUEST_URI: {}",request.getAttribute(ERROR_REQUEST_URI));
log.info("ERROR_SERVLET_NAME: {}",request.getAttribute(ERROR_SERVLET_NAME));
log.info("ERROR_STATUS_CODE: {}",request.getAttribute(ERROR_STATUS_CODE));
log.info("dispatchType={}", request.getDispatcherType());

 

 

여기서 우리가 알아볼 건 마지막에 dispatchType이다

DispatchType은 

고객에서 처음 요청이 들어오면 WAS는 request의 dispatchType을 REQEUST로 설정한다

오류 페이지를 위해 WAS에서 요청을 호출하면 이때는 dispatchType = ERROR이다

 

DispatcherType의 결과 

  • REQUEST : 클라이언트 요청
  • ERROR : 오류 요청
  • FORWARD : MVC에서 배웠던 서블릿에서 다른 서블릿이나 JSP를 호출할
  • INCLUDE : 서블릿에서 다른 서블릿이나 JSP 결과를 포함할 때
  • ASYNC : 서블릿 비동기 호출

 

이 정보를 활용해 Filter 가 2번 타지는 문제를 해결할 수 있다

 

이제 인터셉터가 2번 타지는 문제를 해결해 보자 

인터셉터에 경우 DispatcherType이라는 건 없다 대신에 인터셉터를 등록할 때 excludePathPatterns을 쓰면 된다

@Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor())
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns("/css/**", "*.ico", "/error", "/error-page/**");
    }

 

 

 

 

 

전체 흐름 정리

  • 필터는 DispatchType으로 중복 호출 제거 (dispatchType=REQUEST)
     → 요청을 다 없애는 게 아니라 dispatchType 이 REQUEST 인 것만 필터 통과 시키고 나머지는 그냥 필터 없이 넘기겠다
  • 인터셉터는 경로 정보(excludePathPatterns)로 중복 호출 제거
1. WAS(/error-ex, dispatchType=REQUEST) -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러

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

3. WAS 오류 페이지 확인

4. WAS(/error-page/500, dispatchType=ERROR) -> 필터(x) -> 서블릿 -> 인터셉터(x) -> 
컨트롤러(/error-page/500) -> View

 

 


스프링 부트의 오류페이지

지금 까지 위에서 쓴 내용은 서블릿이 예외페이지를 처리하는 과정이다.

  • WebServerCustomizer를 만들고
  • 예외 종류에 따라서 ErrorPage를 추가하고
  • 예외 처리용 Controller를 만들었다

서블릿의 이러한 문제점을 인지하고 고대 개발자들이 SpringBoot를 만들 때 해당 내용을 해결해 주었다 

이번에도 마찬가지다!! 헤헿.. 스프링 부트는 이런 과정을 모두 기본으로 제공한다

 

 

특정 위치의 뷰 템플릿과 템플릿 이름을 만들기만 하면 스프링이 알아서 해당 위치로 오류페이지를 보내준다

 

 

 

 

 

BasicErrorController와 ErrorPage View

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

  • ErrorPage를 자동으로 등록한다. 이때 /error라는 경로로 기본 오류 페이지를 설정한다
    • WAS까지 전달 됐는데 ErrorPage 보낼 때가 없으면 /error로 보낸다(default 오류페이지)
    • 서블릿 밖으로 예외가 발생하거나 , response.sendError(…..)가 호출되면 모든 오류는 /error를 호출하게 된다
  • BasicErrorController라는 스프링 컨트롤러를 자동으로 등록한다
    • /error를 매핑해서 처리하는 컨트롤러다

 

⇒ 정리하면 /error 경로로 오류페이지를 제공하고 BasicErrorController 가 이 경로의 Controller로 제공된다

개발자는 오류 페이지 화면 BasicErrorController에 우선순위와 룰에 따라서 만들면 된다.

 

 

 

 

 

오류페지이와 페이지 이름

오류 페이지는 resources/template/error 디렉터리에 만들면 된다 

뷰의 이름은 오류 코드로 지으면 된다 

 

ex) resources/template/error 경로에 

400.html

404.html

500.html

 

심지어는 4xx.html 이렇게 400대 에러를 전부 처리하는 페이지를 만들 수도 있다 

 

 

 

뷰 선택 우선순위

  1. 뷰템플릿을 찾아본다 (template/error) 디렉터리 안에 상태코드별로 html (400.html, 4xx.html,500.html..)
  2. 정적 리소스를 찾아본다
  3. 적용 대상이 없을 땐 기본 error 페이지를 호출한다

 

 

BasicErrorController 가 제공하는 기본 정보들

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

 

 

 

 

 

주의

BasicErrorController는 예외나 Error 가 WAS까지 올라가고 거기서 reqeust에 attribute를 담아서 /error 보내는 것

그래서 이전에 필터가 두 번 불리는 문제 , 인터셉터가 두번 불리는 문제는 우리가 직접 WebConfig에서 해결해야 한다

필터는 DispatchType으로 인터셉터는 ExcludeURIpatters로 해결했다

 

 


결론

동적 HTML을 제공해야 하는 MVC 상황이라면 BasicController를 사용하고 API를 사용해야 하는 상황이라면 이후 블로그에 올릴 @ExceptionHandler를 이용하자

 

사용하는 걸 잘하는 것도 중요하지만 내부에 이런 동작들에 의해서 스프링이 마법같이 편하게 동작해준다는 걸 아는 것도 중요하다

@ExceptionHandler로 바로 사용하는 방법을 말하는 것보단 김영한 강사님의 강의 스타일처럼 밑에서부터 차근차근 따라가면서 정리해 보겠다!

'🌿 스프링 > 스프링 MVC 2편' 카테고리의 다른 글

파일 업로드  (0) 2026.02.15
API 예외처리  (0) 2026.02.15
서블릿 필터와 스블릿 인터셉터  (0) 2026.02.15
로그인 처리- 쿠키,세션  (0) 2026.02.14
Validation  (0) 2026.02.14