8. 예외 처리와 오류 페이지

ChoiJaewoo·2026년 2월 8일

Spring MVC 2편

목록 보기
8/10

예외 처리와 오류 페이지

  • 프로젝트 생성
  • 서블릿 예외 처리 - 시작
  • 서블릿 예외 처리 - 오류 화면 제공
  • 서블릿 예외 처리 - 오류 페이지 작동 원리
  • 서블릿 예외 처리 - 필터
  • 서블릿 예외 처리 - 인터셉터
  • 스프링부트 - 오류 페이지 1
  • 스프링부트 - 오류 페이지 2

프로젝트 생성

  • Dependencies: Spring Web, Lombok , Thymeleaf, Validation

서블릿 예외 처리 - 시작

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

  1. Exception : 예외
  2. response.sendError(HTTP 상태 코드, 오류 메시지)

Exception (예외) ?

자바 직접 실행

자바의 메인 메서드를 직접 실행하는 경우, main이라는 이름의 쓰레드가 실행된다.

실행 도중에 예외를 잡지 못하고, 처음 실행한 main() 메서드를 넘어서 예외가 던져지면, 예외 정보를 남기고, 해당 쓰레드는 종료된다.

웹 애플리케이션

웹 애플리케이션은 사용자 요청별로 별도의 쓰레드가 할당되고, 서블릿 컨테이너 안에서 실행된다.

애플리케이션에서 예외가 발생했는데, 어디선가 try-catch 로 예외를 잡아서 처리한다면 아무런 문제가 없다. 그런데 만약에 애플리케이션에서 예외를 잡지 못하고, 서블릿 밖까지 예외가 전달되면??

WAS <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러

컨트롤러에서 예외가 발생하고, 이것이 WAS 까지 왔다면.

톰캣과 같은 WAS 까지 예외가 전달된다. WAS 는 예외가 올라오면 어떻게 처리할까?

ServletExController

@Slf4j
@Controller
public class ServletExController {
    @GetMapping("/error-ex")
    public void errorEx() {
        throw new RuntimeException("에외 발생!");
    }
    @GetMapping("/error-404")
    public void error404(HttpServletResponse response) throws IOException {
        response.sendError(404, "404 오류!");
    }

    @GetMapping("error-500")
    public void error500(HttpServletResponse response) throws IOException {
        response.sendError(500);
    }
}

http://localhost:8080/error-ex 요청 시 500 - Internal Server Error가 발생한다.

http://localhost:8080/error-404 요청 시 404 에러 코드가 발생함.

http://localhost:8080/error-500 요청 시 500 에러 코드가 발생함.

서블릿 예외 처리 - 오류 화면 제공

서블릿 컨테이너가 제공하는 기본 예외 처리 화면은 고객 친화적이지 않다. 서블릿이 제공하는 오류 화면 기능을 사용해보자.

서블릿은 Exception (예외) 가 발생해서, 서블릿 밖으로 전달되거나 또는 response.sendError() 가 호출 되었을 때 각각의 상황에 맞춘 오류 처리 기능을 제공한다.

서블릿 예외 처리 - 오류 페이지 작동 원리

서블릿은 Exception 이 발생해서 서블릿 밖으로 전달되거나 또는 response.sendError() 가 호출 되었을 때, 설정된 오류 페이지를 찾는다.

예외 발생 흐름

WAS <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러

컨트롤러에서 예외가 발생 → WAS 까지 전달

sendError 흐름

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

이후 WAS는 해당 예외를 처리하는 오류 페이지 정보를 확인한다.

ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-page/404");

HttpStatus.NOT_FOUND 예외가 WAS까지 전달되면, 그에 맞는 오류페이지로 /error-page/404 가 지정되어 있다.

WAS는 오류 페이지를 출력하기 위해 /error-page/404 를 다시 요청한다. 이후 컨트롤러에서 오류페이지를 띄운다(View 호출)

WAS "/error-page/404" 다시 요청 -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러 ("/error-page/404")
-> View  

💡 웹 브라우저 (클라이언트) 는 서버 내부에서 이런일이 일어나는지 전혀 모른다.

오류 정보 추가

WAS는 오류페이지를 단순히 다시 요청만 하는것이 아니라, 오류 정보를 request의 attribute에 추가해서 넘겨준다.

ErrorPageController - 오류 출력

@Slf4j
@Controller
public class ErrorPageController {
    //RequestDispatcher 상수로 정의되어 있음
    public static final String ERROR_EXCEPTION =
            "jakarta.servlet.error.exception";
    public static final String ERROR_EXCEPTION_TYPE =
            "jakarta.servlet.error.exception_type";
    public static final String ERROR_MESSAGE = "jakarta.servlet.error.message";
    public static final String ERROR_REQUEST_URI =
            "jakarta.servlet.error.request_uri";
    public static final String ERROR_SERVLET_NAME =
            "jakarta.servlet.error.servlet_name";
    public static final String ERROR_STATUS_CODE =
            "jakarta.servlet.error.status_code";

    @RequestMapping("/error-page/404")
    public String errorPage404(HttpServletRequest request, HttpServletResponse response) {
        log.info("errorPage 404");
        printErrorInfo(request);
        return "error-page/404";
    }

    @RequestMapping("/error-page/500")
    public String errorPage500(HttpServletRequest request, HttpServletResponse response) {
        log.info("errorPage 500");
        printErrorInfo(request);
        return "error-page/500";
    }

    private void printErrorInfo(HttpServletRequest request) {
        log.info("ERROR EXCEPTION: ex : {}", 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());
    }
}

서블릿 예외 처리 - 필터

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 {
        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);
        }
    }
    @Override
    public void destroy() {
        log.info("log filter destroy");
    }

}

WebConfig

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Bean
    public FilterRegistrationBean logFilter() {
        FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
        filterFilterRegistrationBean.setFilter(new LogFilter());
        filterFilterRegistrationBean.setOrder(1);
        filterFilterRegistrationBean.addUrlPatterns("/*");
        filterFilterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
        return filterFilterRegistrationBean;
    }
}

http://localhost:8080/error-ex → 요청

실행 과정

  1. LogFilter에서 로그 출력

    REQUEST [5a44ed08-cc80-4a68-b126-f8192d76714b][REQUEST][/error-ex] → LogFilter

  2. 서블릿 호출

  3. 컨트롤러 errorEx() 컨트롤러에서 RuntimeException(”예외 발생!”) 호출

  4. 서블릿으로 예외 전달

  5. 필터로 예외 전달

  6. 필터에서 예외 catch 이후 로그 출력 :

    EXCEPTION Request processing failed: java.lang.RuntimeException: 에외 발생!

  7. finally 로그 출력

    RESPONSE [5a44ed08-cc80-4a68-b126-f8192d76714b][REQUEST][/error-ex]

  8. WAS 로 예외 전달

  9. WebServerCustomizer 에서
    ErrorPage errorPageEx = new ErrorPage*(*RuntimeException.class, "/error-page/500"*)*;

    /error-page/500 으로 요청
  10. LogFilter에서 로그 다시 출력됨.

    REQUEST [609391c2-4820-4cd0-b64d-004c3279ab8e][ERROR][/error-page/500]

  11. 서블릿 호출

  12. 컨트롤러 errorPage500 호출 이후

    로그 출력됨.

    errorPage 500

    ERROR EXCEPTION: java.lang.RuntimeException: 에외 발생!

    …

    dispatchType=ERROR

  13. 뷰 렌더링 “error-page/500”

  14. 서블릿 호출

  15. 필터 호출 - 예외 없음.

  16. finally 로그 출력

    RESPONSE [f9fd2bed-5dc4-4b6a-a1d0-ba2d93fc0fb3][ERROR][/error-page/500]

→ 즉, Filter는 2번 호출됨.

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

Filter → Servlet 기술이지만, Interceptor → Spring 기술이다,

Interceptor에서 예외 처리는 어떻게 이루어질까?

인터셉터 중복 호출 제거

LogInterceptor

@Slf4j
public class LogInterceptor implements HandlerInterceptor {
    public static final String LOG_ID = "logId";
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse
            response, Object handler) throws Exception {
        String requestURI = request.getRequestURI();
        String uuid = UUID.randomUUID().toString();
        request.setAttribute(LOG_ID, uuid);
        log.info("REQUEST  [{}][{}][{}][{}]", uuid, request.getDispatcherType(),
                requestURI, handler);
        return true;
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse
            response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("postHandle [{}]", modelAndView);
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse
            response, Object handler, Exception ex) throws Exception {
        String requestURI = request.getRequestURI();
        String logId = (String)request.getAttribute(LOG_ID);
        log.info("RESPONSE [{}][{}][{}]", logId, request.getDispatcherType(),
                requestURI);
        if (ex != null) {
            log.error("afterCompletion error!!", ex);
        }
    }
}

preHandle → 실행 됨

controller → 예외 발생 함

postHandle → controller 에서 예외 발생하여 실행 안됨.

afterCompletion → 항상 호출 됨.

Interceptor를 내부 호출 시 호출 하지 않기 위해서는

.excludePathPatterns*(*"/css/**", "*.ico", "/error", "/error-page/**"*)*;

에서 “/error-page/**” 를 넣어줘야 한다.

WebConfig - addInterceptors

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new LogInterceptor())
            .order(1)
            .addPathPatterns("/**")
            .excludePathPatterns("/css/**", "*.ico", "/error", "/error-page/**");
    //필터 처럼 Dispatcher Type 에 따라 적용할 지 선택할 수 없다.
    //대신 오류 경로를 추가하면 된다.
}

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

이제는 서블릿이 제공하는 기능을 넘어서, 스프링 부트가 오류페이지 같은 것들을 어떻게 제공하는지 알아보자.

지금까지 예외 처리를 위해

  • WebServerCustomizer를 만들고
  • 예외 종류에 따라서 ErrorPage를 추가하고
    • 예외 처리용 컨트롤러 ErrorPageController를 만들었다.

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

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

즉, 에러가 발생하면, “/error” 를 기본 요청하고, 이를 처리하는 컨트롤러는 BasicErrorController 이다.

개발자는 오류페이지만 등록하면 된다. → 어디에?

View 선택 우선 순위

  1. View templates
    • resources/templates/error/500.html → 5xx 보다 우선순위가 높다.
    • 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)
    1. resources/templates/error.html

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

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

application.properties

server.error.include-exception=true
server.error.include-message=on_param
server.error.include-stacktrace=on_param
server.error.include-binding-errors=on_param

이를 통해 오류 정보를 model 에 포함할지 여부를 선택할 수 있다.

🚨 하지만, 실무에서는 노출하면 안된다. - 보안에 취약점, 고객이 이해할 수 있는 간단한 오류 메시지 만 포함해야 한다.

0개의 댓글