[Spring MVC] 예외 처리

jiyoon·2023년 5월 21일
1

예외 처리란?

클라이언트 요청 시 데이터의 유효성 검증에 실패할 경우 Response Body의 내용만으로 어떤 항목이 유효성 검증에 실패했는지 알 수가 없다. 이에 따라 클라이언트 쪽에서 에러메시지를 조금 더 구체적으로 알 수 있도록 바꾸는 작업으로 예외 처리를 진행한다.


1.@RestControllerAdvice 애너테이션을 추가하면 여러 개의 Controller 클래스에서 @ExceptionHandler, @InitBinder, @ModelAttribute가 추가된 메서드를 공유해서 사용할 수 있다.

-> @InitBinder@ModelAttribue 애너테이션은 JSP, Thymeleaf 같은 서버 사이드 렌더링(SSR, Server Side Rendering) 방식에서 주로 사용되는 방식이다.


@RestControllerAdvice      
public class GlobalExceptionAdvice {
    @ExceptionHandler
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleMethodArgumentNotValidException(
            MethodArgumentNotValidException e) {
        final ErrorResponse response = ErrorResponse.of(e.getBindingResult());

        return response;
    }

    @ExceptionHandler
    public ErrorResponse handleConstraintViolationException(
            ConstraintViolationException e) {

        final ErrorResponse response = ErrorResponse.of(e.getConstraintViolations());

        return response;
    }
}

GlobalExceptionAdvice 클래스에 @RestControllerAdvice 애너테이션을 추가하여 Controller 클래스에서 발생하는 예외를 처리하도록 한다.

  • Controller에서 유효성 검사 오류가 발생하면 MethodArgumentNotValidException, ConstraintViolationException 이 실행된다.

  • 이 예외들은 GlobalExceptionAdvice에서 잡히고 각각 handleMethodArgumentNotValidException과 handleConstraintViolationException 메서드가 처리한다.

  • 이 메서드들에서는 각각 ErrorResponse.of(BindingResult bindingResult) 와 ErrorResponse.of(Set<ConstraintViolation<?>> violations)를 호출하여 ErrorResponse 객체를 생성한다.

  • ErrorResponse 객체를 생성할 때, 내부적으로 FieldError.of(BindingResult bindingResult) 또는 ConstraintViolationError.of(Set<ConstraintViolation<?>> violations)를 호출하여 각각의 오류 목록을 만든다.

  • 마지막으로 ErrorResponse 객체가 HTTP 응답으로 반환된다. 클라이언트 측에 어떤 입력 값이 왜 잘못되었는지 알릴 수 있다.

@Getter
public class ErrorResponse {
    private List<FieldError> fieldErrors;
    private List<ConstraintViolationError> violationErrors;

    private ErrorResponse(List<FieldError> fieldErrors, List<ConstraintViolationError> violationErrors) {
        this.fieldErrors = fieldErrors;
        this.violationErrors = violationErrors;
    }

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

    public static ErrorResponse of(Set<ConstraintViolation<?>> violations) {
        return new ErrorResponse(null, ConstraintViolationError.of(violations));
    }

    @Getter  // 필드(DTO 클래스의 멤버 변수)의 유효성 검증에서 발생하는 에러 정보를 생성한다.
    public static class FieldError {
        private String field;
        private Object rejectedValue;
        private String reason;

        public FieldError(String field, Object rejectedValue, String reason) {
            this.field = field;
            this.rejectedValue = rejectedValue;
            this.reason = reason;
        }

        public static List<FieldError> of(BindingResult bindingResult) {
            final List<org.springframework.validation.FieldError> fieldErrors =
                    bindingResult.getFieldErrors();

            return fieldErrors.stream()
                    .map(fieldError -> new FieldError(
                            fieldError.getField(),
                            fieldError.getRejectedValue() == null ?
                                    "" : fieldError.getRejectedValue().toString(),
                            fieldError.getDefaultMessage()))
                    .collect(Collectors.toList());
        }
    }

    @Getter  // URI 변수 값에 대한 에러 정보를 생성한다.
    public static class ConstraintViolationError {
        private String propertyPath;
        private Object rejectedValue;
        private String reason;

        public ConstraintViolationError(String propertyPath, Object rejectedValue, String reason) {
            this.propertyPath = propertyPath;
            this.rejectedValue = rejectedValue;
            this.reason = reason;
        }

        public static List<ConstraintViolationError> of(
                Set<ConstraintViolation<?>> constraintViolations) {
            return constraintViolations.stream()
                    .map(constraintViolation -> new ConstraintViolationError(
                            constraintViolation.getPropertyPath().toString(),
                            constraintViolation.getInvalidValue().toString(),
                            constraintViolation.getMessage()
                    )).collect(Collectors.toList());
        }

    }

}
private List<FieldError> fieldErrors;

-> MethodArgumentNotValidException 으로 부터 발생하는 에러 정보를 담는 멤버 변수이다., DTO 멤버 변수 필드의 유효성 검증 실패로 발생한 에러 정보를 담는 멤버 변수이다.
private List<ConstraintViolationError> violationErrors;

-> ConstraintViolationException 으로 부터 발생하는 에러 정보를 담는 멤버 변수이다. 
URI 변수 값의 유효성 검증에 실패로 발생한 에러 정보를 담는 멤버 변수이다.
private ErrorResponse(List<FieldError> fieldErrors, List<ConstraintViolationError> violationErrors) {
        this.fieldErrors = fieldErrors;
        this.violationErrors = violationErrors;
    }
    
 -> private 접근 제한자를 사용하여 ErrorResponse 클래스는 new ErrorResponse 방식으로 객체를 생성할 수 없지만,
 대신에 of() 메서드를 이용해서 ErrorResponse 객체를 생성할 수 있고 ErrorResponse의 객체를 생성함과 동시에 ErrorResponse의 역할을 명확하게 해준다.
public static ErrorResponse of(BindingResult bindingResult) {
        return new ErrorResponse(FieldError.of(bindingResult), null);
    }

-> MethodArgumentNotValidException에 대한 ErrorResponse 객체를 생성해 준다.
MethodArgumentNotValidException 에서 에러 정보를 얻기 위해 필요한 것이 바로 BindingResult 객체이므로,of() 메서드를 호출하는 쪽에서 BindingResult 객체를 파라미터로 넘겨주면 된다.

그런데 이 BindingResult 객체를 가지고 에러 정보를 추출하고 가공하는 일은 ErrorResponse 클래스의 static 멤버 클래스인
FieldError 클래스에게 위임하고 있다.
public static ErrorResponse of(Set<ConstraintViolation<?>> violations) {
        return new ErrorResponse(null, ConstraintViolationError.of(violations));
    }

-> ConstraintViolationException에 대한 ErrorResponse 객체를 생성해 준다.
   ConstraintViolationException 에서 에러 정보를 얻기 위해 필요한 것이 바로
   Set<ConstraintViolation<?>> 객체이므로 이 of() 메서드를 호출하는 쪽에서 
   Set<ConstraintViolation<?>> 객체를 파라미터로 넘겨주면 된다.
   
   Set<ConstraintViolation<?>> 객체를 가지고 에러 정보를 추출하고 가공하는 일은
   ErrorResponse 클래스의 static 멤버 클래스인 ConstraintViolationError 클래스에게
   위임하고 있다. (ErrorResponse 객체에 여러 정보를 담는 역할이 명확하게 분리된다.)

of() 메서드는 Java 8의 API에서 볼 수 있는 네이밍 컨벤션(Naming Convention)이다. 주로 객체 생성 시 어떤 값들의 (of~) 객체를 생성한다는 의미에서 of() 메서드를 사용한다.

Set<ConstraintViolation<?>> 에서 <?>Java의 제네릭 타입 ConstraintViolation<?>을 의미하고 '<?>' 는 와일드카드(wildcard)를 나타낸다.

와일드 카드는 어떤 타입이드 가능함을 의미함.

Set<ConstraintViolation<?>>"어떤 타입의 ConstraintViolation 객체든 담을 수 있는 Set" 을 뜻한다.
이렇게 사용하면 ConstraintViolation<String> 이나 ConstraintViolation<Integer> 등 어떤 타입의 ConstraintViolation 객체든 해당 Set에 넣을 수 있다.
profile
한걸음 나아가는 개발자

0개의 댓글