[Spring] 예외처리하기 (ExceptionHandler)

이수민·2023년 1월 23일
1

spring

목록 보기
5/12

스프링에서 예외처리하는 방식에 대해 알아보자.
스프링의 예외처리 방식을 이해하기 위해 먼저 스프링의 전체적인 흐름을 이해해야 한다.

Spring의 요청 처리 과정

  • 위 그림은 Spring이 요청에 대한 처리를 어떠한 흐름으로 진행하는 지에 대한 그림이다. 단순한 순서(흐름)로만 보자.
    (Interceptor가 Controller에게 요청을 위임하는 것은 아니다.)
  • 그림 속 과정에 대한 자세한 소개는 이 블로그에서 확인할 수 있다.
  1. 클라이언트의 요청을 디스패처 서블릿이 받음
  2. 요청 정보를 통해 요청을 위임할 컨트롤러를 찾음
  3. 요청을 컨트롤러로 위임할 핸들러 어댑터를 찾아서 전달함
  4. 핸들러 어댑터가 컨트롤러로 요청을 위임함
  5. 비지니스 로직을 처리함
  6. 컨트롤러가 반환값을 반환함
  7. HandlerAdapter가 반환값을 처리함
  8. 서버의 응답을 클라이언트로 반환함

이러한 과정으로 요청 처리가 진행되는데, 만약 Controller에서 로직을 처리하던 중 예상치 못한 에러가 발생한다면 어떻게 될까?


BasicErrorController

  • BasicErrorController란 스프링 부트의 기본 예외처리 Controller이다.

  • 때문에, 별다른 설정을 하지 않았다면 예외가 발생했을 때 BasicErrorController로 예외처리 요청이 전달된다.

예외 발생 시 요청 전달 흐름이다.
WAS -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러
-> 컨트롤러(예외발생) -> 인터셉터 -> 서블릿 -> 필터 -> WAS
-> WAS -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러(BasicErrorController)

컨트롤러와 필터, 인터셉터가 두 번씩 호출되는 것을 알 수 있다. 또한 기본 에러는 클라이언트에게 status code 500에 "Internel server error"로만 응답하기 때문에, 클라이언트는 무슨 에러인지 알 수 없다.

이러한 바람직하지 못한 에러 처리를 위해 우리는 별도의 예외처리 전략을 사용해야 한다.

스프링이 제공하는 예외처리 방법

HandlerExceptionResolver

  • 에러 처리를 메인 로직으로부터 분리함

  • 대부분의 HandlerExceptionResolver는 발생한 Exception을 catch하고 HTTP 상태나 응답 메세지 등을 설정한다.

    WAS는 이를 정상적인 응답으로 인식하여, 위에서 설명한 복잡한 WAS의 에러 전달이 진행되지 않는 것.

HandlerExceptionResolver 구현체

  • 적합한 예외 처리를 위해 HandlerExceptionResolver 구현체들을 빈으로 등록해서 관리한다. 아래는 빈으로 등록된 4가지 구현체들이다. (우선순위 순)

    • DefaultErrorAttributes
      • 에러 속성을 저장하며 직접 예외를 처리하지는 않는다.
        (때문에 얘는 제외하고 직접 예외를 처리하는 3가지 구현체들만 HandlerExceptionResolverComposite로 모아 따로 관리함)
    • ExceptionHandlerExceptionResolver
      • 에러 응답을 위한 Controller나 ControllerAdvice에 있는 ExceptionHandler를 처리함
    • ResponseStatusExceptionResolver
      • Http 상태 코드를 지정하는 @ResponseStatus 또는 ResponseStatusException를 처리함
    • DefaultHandlerExceptionResolver
      • 스프링 내부의 기본 예외들을 처리한다.

Spring에서 ExceptionResolver를 동작시켜 에러를 처리할 때 사용하는 도구들을 살펴보자.

@ExceptionHandler

  • 매우 유연하게 에러처리를 할 수 있는 방법을 제공한다.
  • 에러 응답을 자유롭게 다룰 수 있다.
  • 컨트롤러의 메소드에 해당 애노테이션을 적용할 수 있는데, 이는 전역으로 사용할 수 없다. 전역으로 처리하기 위해서는 @RestControllerAdvice(또는 @ControllerAdvice) 애노테이션을 사용해야 한다.

@RestControllerAdvice

  • Spring 4.3부터 제공하는 애노테이션이다.
  • @ExceptionHandler를 전역적으로 적용할 수 있게 해준다.
  • @ControllerAdvice 와의 차이점은 에러 응답을 JSON으로 내려준다는 것이다.
  • 사용방법 : 애노테이션을 적용해 전역적으로 에러를 핸들링하는 Class를 만들어 사용한다. (에러 처리 위임)

그럼 이제 @ExceptionHandler와 @RestControllerAdvice를 사용해 예외 처리를 하는 간단한 실습을 진행해보자.



예외 처리 적용

  • @Valid 적용 시 유효성 검사 중 에러가 발생한 경우의 예외를 처리해보자.
  • 이를 위해 만들어야 할 것들은 에러코드 정의와 응답 객체 정의, 전역적으로 에러를 핸들링하는 Class이다.

ErrorCode (Enum)

  • 에러 별, Http Status code와 에러 메시지를 정의하는 파일이다.
  • INVALID_INPUT_VALUE는 Validation 예외 때 이용할 상수
// hello.springmvc.first.Exceptions.ErrorCode.java
@Getter
@RequiredArgsConstructor
public enum ErrorCode {
    INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "INVALID INPUT VALUE");

    private final HttpStatus status;
    private final String message;
}

ErrorResponse

  • 클라이언트에게 전달할 에러의 응답 형태를 정의하는 클래스다.
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ErrorResponse {
    private String message;
    private int status;
    private List<FieldError> errors;

    private ErrorResponse(final ErrorCode code) {
        this.message = code.getMessage();
        this.status = code.getStatus().value();
    }

    private ErrorResponse(final ErrorCode code, final List<FieldError> errors) {
        this.message = code.getMessage();
        this.status = code.getStatus().value();
        this.errors = errors;
    }

    public static ErrorResponse of(final ErrorCode code) {
        return new ErrorResponse(code);
    }

    public static ErrorResponse of(final ErrorCode code, final BindingResult bindingResult) {
        return new ErrorResponse(code, FieldError.of(bindingResult));
    }

    @Getter
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    public static class FieldError {
        private String field;
        private String value;

        private FieldError(final String field, final String value) {
            this.field = field;
            this.value = value;
        }

        private static List<FieldError> of(final BindingResult bindingResult) {
            final List<org.springframework.validation.FieldError> fieldErrors = bindingResult.getFieldErrors();
            return fieldErrors.stream()
                    .map(error -> new FieldError(
                            error.getField(),
                            error.getRejectedValue() == null ? "" : error.getRejectedValue().toString()))
                    .collect(Collectors.toList());
        }
    }
}

GlobalExceptionHandler

  • 앞에서 언급한 @RestControllerAdvice, @ExceptionHandler를 사용해 전역 예외 처리를 담당하는 Class이다.
  • MethodArgumentNotValidException : 데이터의 유효성 검사(@Valid)가 실패한 경우 throw되는 Exception이다.
    • 위 에러가 발생했을 때 이 메서드가 에러 처리를 위임한다.
@RestControllerAdvice
public class GlobalExceptionHandler {

    // @Valid 예외 발생
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        final var response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE, e.getBindingResult());
        return new ResponseEntity<>(response, ErrorCode.INVALID_INPUT_VALUE.getStatus());
    }
}


컨퍼런스

https://mangkyu.tistory.com/18
https://mangkyu.tistory.com/173
https://mangkyu.tistory.com/204

profile
BE 개발자를 꿈꾸는 학생입니다🐣

0개의 댓글