본문 바로가기

Spring

Spring에서의 예외 처리 및 에러 페이지

서블릿에서 기본 예외 처리

클라이언트로부터 요청이 들어오면 흐름은 다음과 같습니다.

WAS -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러 (예외 발생)

여기서 예외가 발생하면 throw 된 예외는 다시 아래와 같이 전달됩니다.

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

WAS는 고객에게 응답하기 전에 sendError()가 호출되었는지 확인합니다. 만약 호출되었다면 에러 코드와 매핑된 url정보를 확인하고, 요청을 다시 보냅니다.

WAS (/error-page/404 , dispatcherType.ERROR) -> 필터(x) -> 서블릿 -> 인터셉터(x) -> 컨트롤러 (뷰 반환)
참고 : 에러 요청은 dispatcherType.ERROR로 설정해서 보내기 때문에 필터를 무시하고, 인터셉터에서는 에러 페이지들을 exclude 함으로써 무시할 수 있습니다.

 

하지만, 이 흐름을 위해서 개발자가 예외 코드에 따른 url을 매핑시켜야 하고, 예외 종류에 따라 에러 페이지를 만들고 예외 처리용 컨트롤러를 만들어야 합니다. 다행스럽게도 스프링 부트는 이 모든 걸 대신해줍니다.

 

스프링 부트 - 에러 페이지

스프링 부트는 기본적으로 BasicErrorController를 제공합니다. BasicErrorController를 사용하면 개발자는 BasicErrorController가 제공하는 룰과 우선순위에 맞게 에러 페이지만 만들면 됩니다.

 

BasicErrorController의 View 우선순위

  1. View Templates ( ex : resources/templates/error/500.html )
  2. static resources ( ex : resources/static/error/400.html )
  3. default ( resources/templates/error.html )

위 경로에 view 파일만 만들어놓으면 스프링 부트가 알아서 예외 코드에 맞는 페이지를 보여줍니다.

이렇게 웹 브라우저에 HTML 화면을 제공할 때는 오류가 발생하면 BasicErrorController를 사용하는 게 편합니다. 단순히 에러 코드에 맞는 뷰 화면만 보여주면 되기 때문입니다. 

 

 

 


API 예외 처리

웹 브라우저에서의 예외 처리와 다르게, API는 응답 형식이나 스펙이 다 다릅니다. 따라서, 같은 예외라도 어떤 컨트롤러에서 발생했느냐에 따라서 다르게 응답해야 하는 경우가 많습니다. 이를 위한 에러 처리 방법이 HandlerExceptionResolver입니다.

HandlerExceptionResolver

HandlerExceptionResolver - 인터페이스

public interface HandlerExceptionResolver {
    ModelAndView resolveException(
        HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
}
  • handler : 컨트롤러 정보
  • ex : 컨트롤러에서 발생한 예외

HandlerExceptionResolver를 이용하면 발생하는 예외에 따라서 400, 404 등등 다른 상태 코드로 처리할 수 있고, 에러 메시지나 형식 등을 API 마다 다르게 처리할 수 있습니다.

 

컨트롤러에서 예외가 발생하면 이렇게 HandlerExceptionResolver에서 예외를 처리합니다. 따라서, 예외가 발생해도 WAS까지 예외가 전달되지 않고 스프링 MVC에서 예외 처리는 끝이 납니다. 결과적으로 WAS 입장에서는 정상 처리가 된 것입니다. WAS까지 예외가 전달되면 추가 프로세스를 실행해야 하지만, HandlerExceptionResolver를 활용하면 이처럼 예외 처리를 깔끔하게 해결할 수 있습니다.

스프링이 제공하는 HandlerExceptionResolver

스프링이 기본적으로 제공하는 HandlerExceptionResolver들은 HandlerExceptionResolverComposite에 다음과 같은 우선순위를 가지고 등록되어 있습니다.

  1. ExceptionHandlerExceptionResolver
  2. ResponseStatusExceptionResolver
  3. DefaultHandlerExceptionResolver

이 중에서 가장 많이 쓰이고, 우선순위 1순위로 적용되는 ExceptionHandlerExceptionResolver 사용 방식을 알아보겠습니다.

@ExceptionHandler

스프링은 API 예외 처리를 위해 @ExceptionHandler 애노테이션을 제공합니다. 이 애노테이션이 바로 1순위로 적용되었던 ExceptionHandlerExceptionResolver입니다. 

 

1. 일관된 에러 메시지 형식으로 응답하기 위해 공통 ErrorResult를 생성합니다.

@Data
@AllArgsConstructor
public class ErrorResult {
    private String code;
    private String message;
}

 

2. Custom 에러를 만듭니다.

public class UserException extends RuntimeException{
    public UserException(String message) {
        super(message);
    }
}

 

3. @ExceptionHandler 구현

@RestControllerAdvice(basePackages = "hello.exception.api")
public class ExControllerAdvice {

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(IllegalArgumentException.class)
    public ErrorResult illegalExHandler(IllegalArgumentException e) {
    	log.error("[ExceptionHandler] ex ", e);
        return new ErrorResult("BAD", e.getMessage());
    }

    @ExceptionHandler
    public ResponseEntity<ErrorResult> userHandler(UserException e){
    	log.error("[ExceptionHandler] ex ", e);
        ErrorResult userEx = new ErrorResult("USER_EX", e.getMessage());
        return new ResponseEntity<>(user_ex, HttpStatus.BAD_REQUEST);
    }

    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler
    public ErrorResult exHandler(Exception e) {
    	log.error("[ExceptionHandler] ex ", e);
        return new ErrorResult("EX", "내부 오류");
    }
}
  • @ControllerAdvice는 대상으로 지정한 여러 컨트롤러에 @ExceptionHandler , @InitBinder 기능을
    부여해주는 역할을 해줍니다. 패키지를 지정하거나, 특정 애노테이션을 타겟으로 정할 수 있습니다.
  • @ExceptionHandler에 예외를 생략할 수 있습니다. 생략하면 메서드 파라미터의 예외가 지정됩니다.
  • ResponseEntity를 반환하면 HTTP 응답 코드를 동적으로 변경할 수 있습니다.
  • @ResponseStatus 애노테이션으로는 HTTP 응답 코드를 동적으로 변경할 수 없습니다.

 

에러 발생

@RestController
public class ApiExceptionController {

    @GetMapping("/api3/members/{id}")
    public MemberDto getMember(@PathVariable("id") String id) {
        if (id.equals("user-ex")) {
            throw new UserException("사용자 오류");
        }
        return new MemberDto(id, "hello" + id);
    }
}

 

실행 흐름

  • 컨트롤러를 호출한 결과 UserException예외가 컨트롤러 밖으로 throw 됩니다.
  • 예외가 발생했으므로 ExceptionResolver가 작동하고, 제일 우선순위가 높은 ExceptionHandlerExceptionResolver가 실행됩니다.
  • ExceptionHandlerExceptionResolver는 해당 컨트롤러에서 발생한 UserException을 처리할 수 있는 @ExceptionHandler가 있는지 확인합니다.
  • userHandler()를 실행하여 예외를 처리합니다. 

정리

웹 브라우저에서 예외가 발생할 때는 BasicErrorController를 사용하면 개발자는 에러 페이지만 만들면 됩니다.

API에서의 예외는 @ControllerAdvice와 @ExceptionHandler를 활용하여 예외를 글로벌하게 처리할 수 있고, 예외마다 서로 다른 응답 결과를 만들 수 있습니다.

 

 

 

참고

https://leeys.tistory.com/30
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2
https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc

'Spring' 카테고리의 다른 글

AOP가 동작하는 원리  (0) 2021.11.11
Parameter를 원하는 대로~♪♫ : ArgumentResolver 정리  (0) 2021.09.09