ysk(0soo)
Lifealong
ysk(0soo)
전체 방문자
오늘
어제
  • 분류 전체보기 (238)
    • Java (50)
      • whiteship-java-study (11)
      • Java (28)
      • time (6)
    • Spring (68)
      • JPA (15)
      • Spring (1)
      • SpringBoot (1)
      • SpringMVC (6)
      • Spring Security (22)
      • Jdbc (1)
      • RestDocs (14)
      • log (6)
    • Kotlin (3)
    • Web (2)
      • nginx (1)
    • Database (14)
      • MySQL (5)
      • PostgreSQL (1)
      • SQL (1)
      • Redis (4)
    • C, C++ (0)
    • Git (1)
    • Docker (2)
    • Cloud (3)
      • AWS (3)
    • 도서, 강의 (0)
      • t5 (0)
    • 기타 (7)
      • 프로그래밍 (1)
    • 끄적끄적 (0)
    • CS (14)
      • 운영체제(OS) (2)
      • 자료구조(Data Structure) (9)
    • 하루한개 (12)
      • 우아한 테크코스-10분테코톡 (12)
    • 스터디 (12)
      • 클린 아키텍처- 로버트마틴 (2)
      • JPA 프로그래밍 스터디 (10)
    • 테스트 (34)
      • JUnit (19)
      • nGrinder (2)
      • JMeter (0)
    • Infra (3)
    • 프로그래머스 백엔드 데브코스 3기 (0)
    • 디자인 패턴 (3)
    • Issue (4)
    • system (1)
      • grafana (0)
      • Prometheus (0)

블로그 메뉴

  • 홈
  • 태그
  • 방명록
  • github

공지사항

인기 글

태그

  • UserDetailsService
  • StructuredConcorrency
  • 구조화된 동시성
  • 동일성
  • 정규표현식
  • 가상 스레드
  • 동시성 제어
  • AccessDecisionVoter 커스텀
  • 트랜잭션
  • java
  • node exporter basic auth
  • DataJpaTest
  • restdocs custom
  • mysql
  • FilterSecurityInterceptor
  • AccessDecisionManager
  • querydsl
  • scope value
  • junit5
  • jpa
  • VirtualThread Springboot
  • nGrinder
  • 인가(Authorization) 처리
  • nginx basic auth
  • 동등성
  • restdocs enum
  • AuthenticationException
  • LocalDateTime
  • 가상 스레드 예외 핸들링
  • tree

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
ysk(0soo)

Lifealong

Spring/SpringMVC

Custom Bean Validation (Custom Validation)

2022. 12. 29. 14:05

Spring MVC Custom Validation

사용 가능한 constraint 어노테이션이 제공하는 제약 조건 외에 필요한 경우, 직접 만들어서 사용할 수 있다.

임의의 제약(Constraint)과 검증자(Validator)를 구현하여 사용하면 된다.

CustomValidation을 위한 Annotation을 생성한다.

@Documented
@Constraint(validatedBy = ContactNumberValidator.class)
@Target( { ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface ContactNumberConstraint {
    String message() default "Invalid phone number";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

@Constraint 주석을 사용하여 필드의 유효성을 검사할 클래스를 정의.

message() 는 사용자 인터페이스에 표시되는 오류 메시지 .

마지막으로 추가 코드는 대부분 Spring 표준을 준수하기 위한 상용구 코드이다.

Validator 만들기

public class ContactNumberValidator implements 
  ConstraintValidator<ContactNumberConstraint, String> {

    @Override
    public void initialize(ContactNumberConstraint contactNumber) {
    }

    @Override
    public boolean isValid(String contactField,
      ConstraintValidatorContext cxt) {
        return contactField != null && contactField.matches("[0-9]+")
          && (contactField.length() > 8) && (contactField.length() < 14);
    }

}

유효성 검사 클래스는 ConstraintValidator 인터페이스를 구현하고 isValid 메서드도 구현해야 한다.

isValid 메서드에서 유효성 검사 규칙을 정의하면 된다 .

ConstraintValidator의 구현은 다음 제한 사항을 준수해야 한다.

  • 객체는 매개변수화되지 않은 유형으로 확인되어야 합니다.
  • 개체의 일반 매개변수는 제한되지 않은 와일드카드 유형이어야 합니다.

이 어노테이션을 사용하고자 하는 FIELD에 정의하면 된다

커스텀 클래스 수준 유효성 검사 Custom Class Validation

클래스에 적용할 수 있는 FieldsValueMatch 라는 새 annotaion을 추가한다 .

주석에는 비교할 필드의 이름을 나타내는 두 개의 매개변수( field 및 fieldMatch) 가 있다.

Custom Annotation 만들기

@Constraint(validatedBy = FieldsValueMatchValidator.class)
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface FieldsValueMatch {

    String message() default "Fields values don't match!";

    String field();

    String fieldMatch();

    @Target({ ElementType.TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    @interface List {
        FieldsValueMatch[] value();
    }
}

사용자 지정 주석에는 클래스에서 여러 FieldsValueMatch 주석을 정의하기 위한 List 하위 인터페이스 도 포함되어 있음을 볼 수 있다.

Custom Validator 만들기

public class FieldsValueMatchValidator 
  implements ConstraintValidator<FieldsValueMatch, Object> {

    private String field;
    private String fieldMatch;

    public void initialize(FieldsValueMatch constraintAnnotation) {
        this.field = constraintAnnotation.field();
        this.fieldMatch = constraintAnnotation.fieldMatch();
    }

    public boolean isValid(Object value, 
      ConstraintValidatorContext context) {

        Object fieldValue = new BeanWrapperImpl(value)
          .getPropertyValue(field);
        Object fieldMatchValue = new BeanWrapperImpl(value)
          .getPropertyValue(fieldMatch);

        if (fieldValue != null) {
            return fieldValue.equals(fieldMatchValue);
        } else {
            return fieldMatchValue == null;
        }
    }
}

어노테이션 적용

@FieldsValueMatch.List({ 
    @FieldsValueMatch(
      field = "password", 
      fieldMatch = "verifyPassword", 
      message = "Passwords do not match!"
    ), 
    @FieldsValueMatch(
      field = "email", 
      fieldMatch = "verifyEmail", 
      message = "Email addresses do not match!"
    )
})
public class NewUserForm {
    private String email;
    private String verifyEmail;
    private String password;
    private String verifyPassword;

    // standard constructor, getters, setters
}

사용자 등록에 필요한 데이터를 위한 NewUserForm 모델 클래스이다 .

두 개의 값을 다시 입력하기 위한 두 개의 verifyEmail 및 verifyPassword 속성 과 함께 두 개의 이메일 및 비밀번호 속성이 있다.

일치하는 해당 필드에 대해 확인할 두 개의 필드가 있으므로 NewUserForm 클래스 에 두 개의 @FieldsValueMatch 어노테이션을 추가한다. 하나는 이메일 값용 이고 다른 하나는 비밀번호 이다 .

Enum Validation 예시

public enum MyColor {
    RED,
    GREEN,
    BLUE;
}

public class DTO {
  //...
      @EnumValid(enumClass = MyColor.class, message = "GREEN은 안됩니다.")
          MyColor favoriteColor;
  //...
}

커스텀 validation annotation

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {EnumValidator.class}) 
public @interface EnumValid {
  // message는 어노테이션에서 지정해주지 않으면 default로 나가거나, properties로 설정해줄 수도 있다.
    String message() default "Invalid value. This is not permitted.";

  // 유효성 검사가 어떤 상황에서 실행되는지 정의할 수 있는 매개변수 그룹
    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    Class<? extends java.lang.Enum<?>> enumClass();
}

커스텀 Validator

public class EnumValidator implements ConstraintValidator<EnumValid, Enum<?>> {
    private EnumValid enumValid;

  // 어노테이션에서 설정한 값을 가져오려면 초기화해주어야 한다. 만약 isValid함수에서 그냥 해결할 수 있으면 안해도됨
    @Override
    public void initialize(EnumValid constraintAnnotation) {
        enumValid = constraintAnnotation;
    }

  // isValid를 오버라이드해서 작성하면 된다.
    @Override
    public boolean isValid(Enum<?> value, ConstraintValidatorContext context) {
        boolean result = true;
        Object[] enumValues = enumValid.enumClass().getEnumConstants();

        if (String.valueOf(value).equals(MyColor.GREEN.toString())) return false;

        return result;
    }
}

예외 처리 Hanlder (ControllerAdvice)

@RestControllerAdvice
public class ExceptionHandler {
  @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidationException(MethodArgumentNotValidException e) {
        ErrorCode errorCode = ErrorCode.INVALID_INPUT_VALUE;
        ErrorResponse er = getErrorResponse(e, errorCode);
        log.error("handleValidationException[{}]", er);
        return ResponseEntity
                .status(errorCode.getStatus())
                .body(er);
    }

    @ExceptionHandler(value = ConstraintViolationException.class)
    public ResponseEntity<ErrorResponse> handleConstraintViolationException(ConstraintViolationException e) {
        ErrorCode errorCode = ErrorCode.INVALID_INPUT_VALUE;
        ErrorResponse er = getErrorResponse(e, errorCode);
        log.error("ConstraintViolationException[{}]", er);
        return ResponseEntity
                .status(errorCode.getStatus())
                .body(er);
    }

    public static ErrorResponse getErrorResponse(BindException e, ErrorCode code) {

        List<ErrorResponse.ValidationError> validationErrorList = e.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(ErrorResponse.ValidationError::of)
                .collect(Collectors.toList());

        return ErrorResponse.builder()
                .code(code.getCode())
                .message(code.getMessage())
                .errors(validationErrorList)
                .status(code.getStatus())
                .build();
    }
}

예외 공통 응답 Response Dto

@Getter
@ToString
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ErrorResponse {
    private int status;
    private String code;
    private String message;

    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    private List<ValidationError> errors;

    @Getter
    @Builder
    @RequiredArgsConstructor
    public static class ValidationError {
        private final String field;
        private final String value;
        private final String message;

        public static ValidationError of(FieldError fieldError) {
            return ValidationError.builder()
                    .field(fieldError.getField())
                    .value(String.valueOf(fieldError.getRejectedValue()))
                    .message(fieldError.getDefaultMessage())
                    .build();
        }

        public static ValidationError of(ConstraintViolation violation) {
            return ValidationError.builder()
                    .field(String.valueOf(violation.getPropertyPath()))
                    .value(String.valueOf(violation.getInvalidValue()))
                    .message(violation.getMessageTemplate())
                    .build();
        }
    }

    @Builder
    public ErrorResponse(int status, String code, String message, List<ValidationError> errors) {
        this.status = status;
        this.code = code;
        this.message = message;
        this.errors = errors;
    }
}

결론

어노테이션의 의도는 숨어있기 때문에 내부적으로 어떤 동작을 하게 되는지 명확하지 않다면 로직 플로우를 이해하기 어렵다.

하물며 ‘커스텀’ 어노테이션은 그 부담을 가중시킬 수 있다.

어노테이션 추가가 당장의 작업 속도를 끌어올릴 순 있지만, 장기적 관점에서 시의적절한 것인지를 공감할 수 있어야 한다.

코드가 간결해진다는 장점 하나만 보고 커스텀 어노테이션을 남용하지 않게 주의해야 한다.

반복적으로 사용하지도 않고, 특정 요청 외에는 사용할 일이 없어 보이는 유효성 검사라면 단순 메서드로 만들어 처리하는 것이 더 좋을 것이다.

커스텀 어노테이션을 잘 이용하면 불필요한 반복코드가 줄어들고, 비즈니스 로직에 더 집중할 수 있다는 장점이 있다.

다만, 커스텀 어노테이션은 의도와 목적을 명확히 하여 구성원간 공감대를 이룬 후 추가하는 것이 좋다.

저작자표시 비영리 (새창열림)

'Spring > SpringMVC' 카테고리의 다른 글

UriComponentsBuilder, ServletUriComponentsBuilder, 201 URI 생성  (0) 2023.02.01
Spring consumes와 produces  (0) 2023.01.25
Java Bean Validation + Validation Tip  (0) 2022.12.29
Bean Validation Annotation 종류  (0) 2022.12.28
Spring MVC의 기본 요청 처리 방식 - Thread Per Request Model  (0) 2022.12.17
    'Spring/SpringMVC' 카테고리의 다른 글
    • UriComponentsBuilder, ServletUriComponentsBuilder, 201 URI 생성
    • Spring consumes와 produces
    • Java Bean Validation + Validation Tip
    • Bean Validation Annotation 종류
    ysk(0soo)
    ysk(0soo)
    백엔드 개발을 좋아합니다. java kotlin spring, infra 에 관심이 많습니다. email : kim206gh@naver.com github : https://github.com/devysk

    티스토리툴바