Personal Research

BindException과 MethodArgumentNotValidException

마손리 2023. 5. 10. 00:29

개인 프로젝트를 하던중 쿼리 파라미터의 정보가 만들어 놓은 Dto의 유효성 검증에 실패했을때 BindException이 발생 하게 되어  @ExceptionHandler를 이용하여 해당 예외에 대한 처리를 코드로 작성 해주려 하고 있었다.

 

 

BindException에 대한 예외 처리 코드 작성

GlobalExceptionAdvice

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

 

ErrorResponse

@Getter
public class ErrorResponse {
    private int status;
    private String message;
    private List<FieldError> fieldErrors;
    private List<ConstraintViolationError> violationErrors;

    ErrorResponse(final List<FieldError> fieldErrors,
                  final List<ConstraintViolationError> violationErrors){
        this.fieldErrors = fieldErrors;
        this.violationErrors = violationErrors;
    }
    public static ErrorResponse of(BindException bindException){
        return new ErrorResponse(FieldError.of(bindException), null);
    }
    public static ErrorResponse of(MethodArgumentNotValidException methodArgumentNotValidException){
        return new ErrorResponse(FieldError.of(methodArgumentNotValidException), null);
    }

    @Getter
    private 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(BindException bindException){
            final List<org.springframework.validation.FieldError> fieldErrors =
                    bindException.getFieldErrors();
            return fieldErrors.stream()
                    .map(e-> new FieldError(
                            e.getField(),
                            e.getRejectedValue() == null ? "" : e.getRejectedValue().toString(),
                            e.getDefaultMessage()))
                    .collect(Collectors.toList());
        }
        public static List<FieldError> of(MethodArgumentNotValidException methodArgumentNotValidException){
            final List<org.springframework.validation.FieldError> fieldErrors =
                    methodArgumentNotValidException.getFieldErrors();
            return fieldErrors.stream()
                    .map(e-> new FieldError(
                            e.getField(),
                            e.getRejectedValue() == null ? "" : e.getRejectedValue().toString(),
                            e.getDefaultMessage()))
                    .collect(Collectors.toList());
        }
    }
    ...
    ...
}

MethodArgumentNotValidException은 이전에 작성한 리퀘스트 바디의 유효성이 실패했을경우 발생하는 예외이며

BindException은 쿼리 파라미터의 유효성이 실패할경우의 예외에 대한 처리 코드이다.

 

문제점 발견

근데 코드를 보면 뭔가 이상하다. 

GlobalExceptionAdvice와 ErrorResponse 코드 모두 메서드 오버로딩을 사용하여 두 예외를 따로 처리하지만 두 객체 모두 getFieldErrors()메서드를 사용하여 같은 처리를 해주고 있다. 

 

 

뭔가 수상하여 MethodArgumentNotValidException의 클래스를 들여다 보니

MethodArgumentNotValidException은 BindException을 상속받고 있었다.

 

 

결국 MethodArgumentNotValidException에 대한 처리를 제거하고 BindException에 대한 처리만 작성해 주어도 두 예외를 모두 처리할 수 있었다.

 

 

개선사항

하지만 위와 같이 BindException에 대한 처리 코드만 작성해주어도 두가지 예외를 처리할수 있게 되었음에도 두가지 모두 같은 응답 형식을 사용하다보니 예외가 발생한 곳이 어딘지 햇갈릴수 밖에 없어 구분을 해주어야 했다.

 

 

ErrorResponse

@Getter
public class ErrorResponse {
    ...
    ...
    @Getter
    private static class FieldError{
        private FieldSource source; // 추가 코드 1 (예외 발생지에 대한 정보 추가)
        private String field;
        private Object rejectedValue;
        private String reason;

        public enum FieldSource{ // 추가 코드 2
            QUERY_PARAMETER("Query Parameter"),
            REQUEST_BODY("Request Body");
            @Getter
            private String source;
            FieldSource(String source) {
                this.source = source;
            }
        }
        public FieldError(FieldSource source, String field, Object rejectedValue, String reason) {
            this.source = source; // 추가 코드 3
            this.field = field;
            this.rejectedValue = rejectedValue;
            this.reason = reason;
        }
        public static List<FieldError> of(BindException bindException){
            final List<org.springframework.validation.FieldError> fieldErrors =
                    bindException.getFieldErrors();
            return fieldErrors.stream()
                    .map(e-> new FieldError(
                            bindException instanceof MethodArgumentNotValidException ? // 추가 코드 4
                                    FieldSource.QUERY_PARAMETER : FieldSource.REQUEST_BODY ,
                            e.getField(),
                            e.getRejectedValue() == null ? "" : e.getRejectedValue().toString(),
                            e.getDefaultMessage()))
                    .collect(Collectors.toList());
        }
    }
    ...
    ...
}

 

ErrorResponse 코드를 위와 같이 조금 변경해 주었다.

예외가 발생한 지점에 대한 정보를 응답해줄 필드를 하나 더 만들고 받은 매개변수 BindException을 instanceof를 사용하여 어떤 객체인지 구분한 뒤 열거형으로 선언된 값을 객체의 종류에 따라 따로 설정해 주었다.

 

 

문제 해결후 결과

코드 수정 이후 예외의 원인을 좀 더 쉽게 파악하게 되었다.

 

 

문제 해결과정

  1. 문제의 발견 : 코드의 중복을 발견
  2. 문제의 원인 : MethodArgumentNotValidException 클래스가 BindException 클래스를 상속받지만 두 클래스의 처리를 따로해주어 코드의 중복이 발생함
  3. 문제의 해결 : 부모 클래스인 BindException 클래스에 대한 코드만 작성해 주어도 MethodArgumentNotValidException에 대한 처리도 함께 이뤄짐
  4. 개선 사항 발견 : 두 클래스에 대한 처리를 같이 하다보니 두 클래스간의 결과물을 구분 할 필요성을 느낌
  5. 개선 사항 해결 : instanceof를 사용하여 두 클래스를 구별하고 새로운 필드를 선언하여 객체에 따라 다르게 초기화 해줌

 

 

 

'Personal Research' 카테고리의 다른 글

SecurityContextHolder  (0) 2023.05.18
Mysql, safe update mode  (0) 2023.03.31
MySQL 서브쿼리  (0) 2023.03.31
실험을 통해 깨닳은 추상화의 중요성  (0) 2023.03.11