사용자가 웹을 이용할 때 접근할 수 없는 페이지나 예기치 못한 오류가 발생했는데, 아무런 안내도 없이 브라우저 기본 오류 화면만 보게 된다면 불쾌감을 느끼게 될 것이다. 이는 곧 서비스 품질이 낮다고 평가받는 원인이 되기도 한다.
따라서 안정적인 웹 서비스를 제공하려면 사용자가 이해할 수 있는 오류 페이지를 준비해야 한다. 오늘은 서블릿에서 제공하는 오류 처리 방식과, 이를 훨씬 간편하게 해주는 스프링 부트의 오류 페이지 처리 기능을 비교하며 살펴볼 것이다.
1. 서블릿 방식 오류 페이지 처리
(1) 오류 페이지 등록하기
스프링 부트 환경에서 서블릿 컨테이너(내장 톰캣 등)를 실행할 때는, WebServerCustomizer를 통해 오류 페이지를 등록할 수 있다.
@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);
}
}
이렇게 등록해두면 response.sendError(404) 또는 response.sendError(500) 호출 시, 지정한 오류 페이지 컨트롤러로 흐름이 전달된다.
(2) 오류 처리 컨트롤러 작성
등록된 경로에 맞는 컨트롤러도 필요하다.
@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";
}
}
그리고 뷰 파일(error-page/404.html, error-page/500.html)을 작성하면 브라우저에 사용자 친화적인 오류 화면을 보여줄 수 있다.
(블로그 가독성을 위해 view 파일은 생략한다.)
(3) WAS 내부 동작 흐름
여기서 중요한 점은 WAS가 오류 페이지를 찾을 때 내부적으로 한 번 더 요청을 발생시킨다는 것이다.
예외 발생 흐름은 다음과 같다.
- 컨트롤러에서 예외 발생 → 필터 → 서블릿 → 인터셉터 → WAS까지 전달
- WAS는 등록된 오류 페이지 매핑 확인
- 다시 /error-page/500 같은 경로로 요청 발생(역순으로 다시 실행)
- 이때 필터와 인터셉터가 다시 실행된다
즉, 한 번의 예외로 인해 불필요하게 필터와 인터셉터가 중복 호출되는 문제가 발생한다.
2. 중복 호출 문제와 DispatcherType
오류 페이지가 호출될 때 WAS 내부에서 한 번 더 요청이 발생한다. 이 과정에서 필터와 인터셉터가 중복 실행될 수 있는데, 비효율 적이기에 이를 구분하기 위해 서블릿은 DispatcherType이라는 개념을 제공한다.
대표적인 타입은 다음과 같다.
- REQUEST : 클라이언트의 정상 요청
- ERROR : 오류 처리 요청
- FORWARD : 다른 서블릿/JSP로 요청을 전달할 때
- INCLUDE : 다른 자원의 결과를 포함할 때
- ASYNC : 비동기 처리
(1) 필터의 경우
필터는 setDispatcherTypes()를 통해 어떤 요청에 동작할지 제어할 수 있다.
기본값은 DispatcherType.REQUEST 이기 때문에, 일반 클라이언트 요청에만 동작하고 오류 요청(ERROR) 에 대해서는 실행되지 않는다.
따라서 별도 설정을 하지 않아도 필터는 중복 호출이 자동으로 막힌다.
filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
이렇게 지정하면 요청뿐 아니라 오류 페이지 호출 시에도 필터가 실행되도록 확장할 수 있다.
(2) 인터셉터의 경우
반면에 인터셉터는 서블릿 스펙이 아니라 스프링 MVC에서 제공하는 기능이다.
따라서 DispatcherType과는 무관하게 항상 동작하며, 오류 요청 시에도 기본적으로 실행된다.
이 문제를 해결하려면 excludePathPatterns("/error-page/**") 와 같이 오류 경로를 명시적으로 제외해야 한다.
3. 서블릿 방식의 한계
서블릿에서 오류 처리를 구현하려면 다음과 같은 과정을 모두 거쳐야 한다.
- WebServerCustomizer로 오류 페이지 등록
- ErrorPageController 작성
- 오류 뷰 파일 작성
세밀한 제어가 가능하다는 장점은 있지만, 설정과 코드가 장황해지고 비효율적이라는 단점이 존재한다.
4. 스프링 부트가 제공하는 오류 페이지 처리
스프링 부트는 이러한 불편함을 줄이기 위해 BasicErrorController를 자동으로 제공한다.
- 모든 오류는 기본적으로 /error 경로로 매핑
- 개발자는 별도의 컨트롤러를 만들 필요 없이 뷰 템플릿만 작성하면 된다
- 뷰 선택 우선순위
- resources/templates/error/500.html, error/404.html 같은 개별 코드 파일
- error/5xx.html, error/4xx.html 같은 범용 코드 파일
- error.html 기본 오류 뷰
예를 들어 resources/templates/error/500.html만 만들어도, 내부적으로 BasicErrorController가 호출되어 오류 화면을 보여준다.
또한 application.properties에서 다음과 같은 옵션으로 오류 정보 노출 여부를 제어할 수도 있다.
server.error.include-message=never
server.error.include-exception=false
server.error.include-stacktrace=never
server.error.include-binding-errors=never
운영 환경에서는 보안과 사용자 경험을 위해 상세한 오류 정보는 숨기고, 단순한 안내 메시지만 보여주는 것이 좋다.
마무리하며:
정리하면, 서블릿 방식은 세밀한 오류 처리 로직을 구현할 수 있다는 장점이 있지만,
컨트롤러 작성, 오류 페이지 등록, 필터/인터셉터 중복 제어까지 모두 직접 신경 써야 해서 상당히 번거롭다.
반면에 스프링 부트는 BasicErrorController를 기본 제공하기 때문에, 개발자는 단순히 오류 화면(HTML 템플릿)만 추가하면 된다. 내부적으로 필요한 로직은 이미 스프링 부트가 처리해주므로 훨씬 간편하다.
즉, 실무에서는 스프링 부트가 제공하는 기본 기능을 적극 활용하고, 필요하다면 커스터마이징하는 것이 가장 효율적인 접근 방식이라 할 수 있다.
다만 여기서 중요한 점은, 우리는 단순히 뷰 파일만 정해진 위치에 만들어두면 오류 페이지가 동작한다는 것이다.
하지만 그 내부에서는 웹 서버와 서블릿 컨트롤러가 어떤 흐름으로 동작하는지를 이해하고 있어야 한다.
왜냐하면, 나중에 문제 상황(예: 오류 페이지가 중복 호출된다거나, 특정 예외가 처리되지 않는다거나)이 발생했을 때, 이 내부 메커니즘을 알고 있다면 문제를 빠르게 인지하고 해결할 수 있기 때문이다
감사합니다.
'스프링' 카테고리의 다른 글
| [스프링] 타입 컨버터 (0) | 2025.10.09 |
|---|---|
| [스프링] API 예외 처리 (0) | 2025.10.05 |
| [스프링] 로그인 처리 3 - 스프링 인터셉터(Interceptor) (0) | 2025.09.29 |
| [스프링] 로그인 처리 2 - 서블릿 필터 (0) | 2025.09.29 |
| [스프링] 로그인 처리 1- 쿠키, 세션 (0) | 2025.09.28 |
