본문 바로가기
Back-End/🌱 Spring Boot (java)

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

by 코딩하는 동현😎 2024. 7. 28.

오류 페이지의 view는 Thymeleaf 템플릿 엔진으로 작성했습니다!

Dependencies: Spring Web, Lombok , Thymeleaf, Validation

 

build.gradle

plugins {
	id 'org.springframework.boot' version '2.5.1'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
}

group = 'hello'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-validation'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
	useJUnitPlatform()
}

스프링이아닌 순수 서블릿 컨테이너는 예외를 Exception(예외), response.sendError(HTTP 상태코드, 오류메세지) 방식으로 처리합니다.

 

자바의 메인 메서드를 실행하면 main()이라는 쓰레드가 실행되는데, 도중에 try~catch문 등으로 예외를 잡지 못하고 main() 쓰레드를 넘어서 예외가 던져지면, 예외 정보를 남기고 해당 쓰레드는 종료됩니다.


웹 애플리케이션

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

애플리케이션에서 예외를 잡지 못하고 서블릿 밖으로 예외가 전달되면 아래와 같이 동작합니다.

 

서버 //  WAS(톰캣까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생) // 클라이언트

WAS(Web Application Server)까지 예외가 전달되는 문제가 생깁니다.

 

이 문제를 이용해서 sendError라는 메서드를 사용해서 해결할 수 있습니다.


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

오류가 발생했을때 HttpServletResponse가 제공하는 sendError메서드를 호출하면 당장 예외가 발생하는것은 아니지만, 서블릿 컨테이너에게 오류가 발생 했다는것을 전달할 수 있고, 예외 발생 시 적절한 HTTP 상태 코드를 전송할 수 있습니다.

 

sendError 메서드를 사용하는 예제를 보여드리기 위해, 아래와 같이 오류를 전달하는 컨트롤러를 생성하겠습니다.

 

ServletExController

package hello.exception.servlet;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@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-400")
    public void error400(HttpServletResponse response) throws IOException {
        response.sendError(400, "400 오류!");
    }

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

 

sendError의 흐름은 아래와 같습니다.

WAS(sendError 호출 기록 확인) <- 필터 <- 서블릿 <- 인터셉터 <- ServletExController(sendError 호출)


오류 페이지 등록

WAS에게 어떤 오류와 코드가 발생했는지 전달하는것은 성공했으니까 그에 맞는 오류페이지를 등록해야됩니다.

스프링 부트가 제공하는 기능을 사용해서 서블릿 오류 페이지를 등록하면 받은 오류 코드에 맞춰서 다시 요청을 해줍니다.

 

WebServerCustomizer

package hello.exception;

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 어노테이션을 추가합니다.
@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");
		// 스프링 컨테이너에게 3개의 오류 페이지를 전달합니다.
        factory.addErrorPages(errorPage404, errorPage500, errorPageEx);
    }
}

 

500 예외가 서버 내부에서 발생한 오류라는 뜻을 포함하고 있기 때문에 여기서는 다른 일반적인 예외가 발생한 경우도 500 오류 화면 으로 처리했습니다.

  • response.sendError(404) : errorPage404 호출
  • response.sendError(500) : errorPage500 호출
  • RuntimeException 또는 그 자식 타입의 예외: errorPageEx 호출

오류가 발생했을 때 처리할 수 있는 컨트롤러도 필요합니다. 예를 들어서 RuntimeException 예외가 발생하면 errorPageEx에서 두번째 파라미터에서 지정한 /error-page/500 이 호출됩니다.

그러면 서버 내부에서 /error-page/500요청이 자체적으로 호출 됩니다.


오류 페이지 컨트롤러

 

ErrorPageController

package hello.exception.servlet;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.RequestDispatcher;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@Controller
public class ErrorPageController {

    //RequestDispatcher 상수로 정의되어 있음
    public static final String ERROR_EXCEPTION = "javax.servlet.error.exception";
    public static final String ERROR_EXCEPTION_TYPE = "javax.servlet.error.exception_type";
    public static final String ERROR_MESSAGE = "javax.servlet.error.message";
    public static final String ERROR_REQUEST_URI = "javax.servlet.error.request_uri";
    public static final String ERROR_SERVLET_NAME = "javax.servlet.error.servlet_name";
    public static final String ERROR_STATUS_CODE = "javax.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";
    }

    @RequestMapping(value = "/error-page/500", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<Map<String, Object>> errorPage500Api(
            HttpServletRequest request, HttpServletResponse response) {

        log.info("API errorPage 500");

        Map<String, Object> result = new HashMap<>();
        Exception ex = (Exception) request.getAttribute(ERROR_EXCEPTION);
        result.put("status", request.getAttribute(ERROR_STATUS_CODE));
        result.put("message", ex.getMessage());

        Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
        return new ResponseEntity<>(result, HttpStatus.valueOf(statusCode));
    }

    private void printErrorInfo(HttpServletRequest request) {
        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());
    }
}

 

WebServerCustomizer에서 받은 요청을 ErrorPageController로 받은후 타임리프 템플릿 경로로 return해서 아래와 같이 작성한 템플릿을 클라이언트에게 보여줍니다.

 

 

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

  • javax.servlet.error.exception : 예외
  • javax.servlet.error.exception_type : 예외 타입
  • javax.servlet.error.message : 오류 메시지
  • javax.servlet.error.request_uri : 클라이언트 요청 URI
  • javax.servlet.error.servlet_name : 오류가 발생한 서블릿 이름
  • javax.servlet.error.status_code : HTTP 상태 코드

스프링 부트 3.0 이상 : 스프링 부트 3.0 이상을 사용한다면 javax 대신에 jakarta 를 사용해야 합니다.

 

 

오류페이지 요청의 흐름은 아래와 같습니다.

WAS (/error-page/500 다시 요청) -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러(/error-page/500) -> 타임리프 View(클라이언트에게 보여주는 페이지)


이 모든 과정의 흐름을 정리하면 아래와 같습니다.

1. WebServerCustomizer <- WAS(sendError 호출 기록 확인) <- 필터 <- 서블릿 <- 인터셉터 <- ServletExController(sendError 호출)

2. WAS (/error-page/500 다시 요청) -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러(/error-page/500) -> 타임리프 View(클라이언트에게 보여주는 페이지)

 

중요한 점은 웹 브라우저(클라이언트)는 2번과 같이 서버내부에서 이런 일이 일어나는지 전혀 모른다는 것이고, 오직 서버 내부에서 오류 페이지를 찾기 위해서 이렇게 추가적인 호출을 합니다.

반응형

댓글