Enthusiasm! Enthusiasm!

스프링 예외처리 본문

자바 스프링/스프링MVC

스프링 예외처리

열동 2023. 3. 15. 01:23

정상호출이 되는 상황을 잘 구현하는 것도 중요하지만 실무에서는 상당히 다양한 예외가 발생하기 때문에 이를 처리하는게 더 어려운 과정일 수 있다. 다양한 예외에 대해서, 클라이언트가 잘못하여 발생한 에러인지, 서버에서 발생한 에러인지 구분하여 사용자에게 잘 전달할 수 있어야 한다. 스프링에서 예외처리를 위해 어떤 기능을 제공하는지 포스팅 하겠다.

우선 스프링이 아닌 순수 서블릿 컨테이너가 예외를 어떻게 처리하는지 간단히 알아보자.

서블릿의 예외 발생과 오류페이지 요청 흐름

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

서블릿이 제공하는 DispatcherType을 이용하면 필터 및 인터셉터의 중복 호출은 막을 수 있다. 하지만 WebServerCustomizer 를 만들고 예외 종류에 따라서 ErrorPage 를 추가하고 예외 처리용 컨트롤러 ErrorPageController 를 만드는 등 복잡한 과정을 거쳐야 하기 때문에 사용하기 어렵다. 스프링 부트는 이런 과정을 모두 기본으로 제공한다!

스프링에서 제공하는 예외처리

스프링 부트에서는 ErrorPage를 자동으로 등록한다. 이때 /error라는 경로로 기본 오류페이지를 설정한다. 또한 BasicErrorController라는 스프링 컨트롤러를 자동으로 등록한다. 개발자는 오류 페이지 화면만 BasicErrorController 가 제공하는 룰과 우선순위에 따라서 등록하면 된다정적 HTML이면 정적 리소스뷰 템플릿을 사용해서 동적으로 오류 화면을 만들고 싶으면 뷰 템플릿 경로에 오류 페이지 파일을 만들어서 넣어두기만 하면 된다.

 

뷰 선택 우선순위

뷰 템플릿이 정적리소스 보다 우선순위가 높으며, 구체적일 수록 우선순위가 높다.

1.resources/templates/error/500.html
2.resources/templates/error/5xx.html
3.resources/static/error/400.html 
4.resources/templates/error.html

 

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는 위와 같은 정보를 model에 담아서 뷰로 전달한다. 뷰 템플릿에서 이를 이용하여 출력할 수 있으며application.properties에서 오류정보를 포함할지 여부를 설정할 수 있다. 하지만 사용자에게 오류 관련 내부 정보들을 노출하는 것은 좋지 않다. 웬만하면 log로 남겨 개발자만 볼 수 있게 설정하자!


API 예외 처리

HTML 페이지의 경우 4xx, 5xx와 같은 오류 페이지만 있으면 대부분의 문제를 해결할 수 있지만 API의 경우에는 각 오류 상황에 맞는 오류 응답 스펙을 정하고, JSON으로 데이터를 내려주어야 하기 때문에 고려할 내용이 더 많다. 이 때 BasicErrorController을 확장하여 JSON을 처리할 수도 있지만 이 방법도 많이 번거롭다. 스프링에서는 이를 해결하기 위해 ExceptionResolver를 제공한다!

ExceptionResolver

ExceptionResolver 적용 후

ExceptionResolver를 사용하면 컨트롤러에서 예외가 발생하여도 ExceptionResolver에서 예외를 처리(해결)해버린다. 따라서 예외가 발생하여도 서블릿 컨테이너 까지 예외가 전달되지 않고, 끝이난다. 결과 적으로 WAS 입장에서는 정상 처리가 되어, 예외상황에서 발생하는 추가 프로세스 실행을 막는다.

스프링이 제공하는 ExceptionResolver

스프링 부트가 기본으로 제공하는 ExceptionResolver는 3가지 이며 우선순위는 다음과 같다.

1. ExceptionHandlerExceptionResolver

2. ResponseStatusExceptionResolver

3. DefaultHandlerExceptionResolver

ResponseStatusExceptionResolver

예외에 따라서 HTTP 상태 코드를 지정해주는 역할을 한다.
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "error.bad")
public class BadRequestException extends RuntimeException {
...
}

위 코드와 같이 @ResponseStatus 애노테이션을 적용하여 상태코드를 설정해줄 수 있다. messages.properties에 error.bad 문자를 등록하여 사용할 수도 있다. 다만 @ResponseStatus는 애노테이션을 직접 넣어줘야하기 때문에 개발자가 직접 변경할 수 없는 예외에는 적용할 수 있다. 이때는 ResponseStatusException를 사용하면 된다. 기능은 같다.

public String responseStatusEx2() {
      throw new ResponseStatusException(HttpStatus.NOT_FOUND, "error.bad", new
  IllegalArgumentException());
  }

DefaultHandlerExceptionResolver

DefaultHandlerExceptionResolver는 스프링 내부에서 발생하는 스프링 예외를 해결한다. 에를들어 파라미터 바인딩 시점에 타입이 맞지 않아 TypeMismatch 에러가 발생했다고 가정하자. 이 경우 예외가 발생했기 때문에 그냥 두면 서블릿 컨테이너까지 오류가 올라가고결과적으로 500 오류가 발생한다. 하지만 이는 서버 오류가 아닌, 클라이언트가 값을 잘못 입력하여 발생한 예외이기 때문에 400대 상태코드를 사용하는게 맞다. 이와 같은 상황에서 DefaultHandlerExceptionResolver는 상태코드를 500 오류에서 400오류로 변경하여 처리한다. 예시뿐만 아니라 스프링 내부 오류를 어떻게 처리할지 수 많은 내용이 정의되어 있다.

ExceptionHandlerExceptionResolver(@ExceptionHandler)

스프링은 API 예외 처리 문제를 해결하기 위해 @ExceptionHandler 라는 애노테이션을 사용하는 매우 편리한 예외 처리 기능을 제공한다. 실무에서 API 예외 처리는 대부분 이 기능을 사용한다고 한다.

 

@ExceptionHandler 예외 처리 방법

@ExceptionHandler 애노테이션을 선언하고, 해당 컨트롤러에서 처리하고 싶은 예외를 지정해주면 된다. 해당 컨트롤러에서 예외가 발생하면 이 메서드가 호출된다. 참고로 지정한 예외 또는 그 예외의 자식 클래스는 모두 잡을 수 있다.

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

 

다음과 같이 여러 예외를 한번에 처리할 수도 있다.

@ExceptionHandler({AException.class, BException.class})
  public String ex(Exception e) {
      log.info("exception e", e);
  }

 

@ExceptionHandler에 예외를 생략하면 메서드 파라미터의 예외가 지정된다!

@ExceptionHandler
  public ResponseEntity<ErrorResult> userExHandle(UserException e) {}

위와 같은 상황에서는 메서드의 파라미터인 UserException에 대한 예외가 지정된다.

 

 

실행흐름

위의 코드블럭중 첫번째인 IllegalArgumentException 예외가 발생했다고 가정하자. 실행 흐름은 다음과 같다.

  •  컨트롤러를 호출한 결과 IllegalArgumentException 예외가 컨트롤러 밖으로 던져진다.
  •  예외가 발생했으로 ExceptionResolver가 작동한다가장 우선순위가 높은 ExceptionHandlerExceptionResolver 가 실행된다.
  •  ExceptionHandlerExceptionResolver 는 해당 컨트롤러에 IllegalArgumentException 을 처리할 수 있는 @ExceptionHandler 가 있는지 확인한다.
  •  illegalExHandle() 를 실행한다@RestController 일때 illegalExHandle() 에도 @ResponseBody 가 적용된다따라서 HTTP 컨버터가 사용되고응답이 다음과 같은 JSON으로 반환된다.
  •  @ResponseStatus(HttpStatus.BAD_REQUEST) 를 지정했으므로 HTTP 상태 코드 400으로 응답한다.

@ControllerAdvice

 정상 코드와 예외 처리 코드를 하나의 컨트롤러에 관리하는것 보다 분리하는게 좋다. @ControllerAdvice 또는 @RestControllerAdvice를 이용하면 둘을 편리하게 분리할 수 있다. 예외 처리 코드를 하나의 클래스에 모아 두 어노태이션 중 하나를 달아주면 애노테이션은 대상으로 지정한 여러 컨트롤러에 @ExceptionHandler @InitBinder 기능을 부여해주는 역할을 한다. 만약 @ControllerAdvice 에 대상을 지정하지 않으면 모든 컨트롤러에 적용된다.


이 포스팅은 Inflearn 김영한님의 스프링 강의 및 강의자료를 참고하여 작성하였습니다.

 

 

'자바 스프링 > 스프링MVC' 카테고리의 다른 글

웹 애플리케이션의 이해  (0) 2023.04.02
스프링 파일 업로드  (0) 2023.03.16
스프링 타입 컨버터  (0) 2023.03.15
스프링 인터셉터  (0) 2023.03.08
검증 - BeanValidation  (0) 2023.03.03
Comments