개발/일지

Hibernate Validation - Custom constrains

Zziny 2023. 1. 23. 09:11

Jakarta Bean Validation API는 표준 제약 조건 주석의 모든 세트를 정의한다.

기본 제약 조건이 충분하지 않은 경우

특정 유효성 검사 요구 사항에 맞는 사용자 지정 제약 조건을 쉽게 만들 수 있다.

Creating a simple constraint

  1. 제약 조건 어노테이션 만들기
  2. validator 구현하기
  3. default 에러 메시지 정의하기

Step 1. 제약 조건 어노테이션 만들기 - Define the actual constraint annotation.

@interface 키워드를 통해 어노테이션 타입으로 정의한다.

public @interface CheckCase {

    String message() default "{org.hibernate.validator.referenceguide.chapter06.CheckCase." +
            "message}";

    Class<?>[] groups() default { };

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

    CaseMode value();
}

어노테이션의 모든 속성은 메서드와 유사한 방식으로 선언된다.

Jakarta Bean Validation API specification에서 요구하는 제약 조건 어노테이션 정의

  • message 속성: 제약 조건을 위반한 경우 오류 메시지를 생성하기 위한 기본 키를 반환
  • groups 속성: 이 제약 조건이 속한 유효성 검사 그룹의 specificaion을 허용, default는 Class<?> 타입의 빈 배열
  • payload 속성: Jakarta Bean Validation API의 클라이언트가 제약 조건에 사용자 지정 페이로드 객체을 할당하는 데 사용할 수 있음. 이 속성은 API 자체에서 사용되지 않음. 사용자 지정 페이로드는 심각도의 정의일 수 있음.
  • value 속성: 필수 케이스 모드를 지정할 수 있음. 지정된 유일한 속성인 경우 특수한 값으로 어노테이션을 사용할 때 생략할 수 있음. 

* payload 속성으로 클라이언트는 인스턴스 유효성 검사 후 ConstraintViolation.getConstraintDescriptor().getPayload()를 사용하여 제약 조건의 심각도에 액세스하고 심각도에 따라 동작을 조정할 수 있다.

public class Severity {
    public interface Info extends Payload {
    }

    public interface Error extends Payload {
    }
}
public class ContactDetails {
    @NotNull(message = "Name is mandatory", payload = Severity.Error.class)
    private String name;

    @NotNull(message = "Phone number not specified, but not mandatory",
            payload = Severity.Info.class)
    private String phoneNumber;

    // ...
}

 

제약 조건 어노테이션에 사용될 수 있는 몇 가지 메타 어노테이션

  • @Target({...})
  • @Retention(RUNTIME)
  • @Constraint(validatedBy = CheckCaseValidator.class)
  • @Documented
  • @Repeatable(List.class)

 

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE })
@Retention(RUNTIME)
@Constraint(validatedBy = CheckCaseValidator.class)
@Documented
@Repeatable(List.class)
public @interface CheckCase {
	//..
}

@Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE})

제약 조건에 대해 지원되는 대상 요소 타입을 정의한다.

  • FIELD: 요소 타입
  • METHOD: 메서드 반환 값
  • PARAMETER: 메서드/생성자 매개변수
  • TYPE_USE: 매개변수화된 타입의 타입 인수(type argument of parameterized types)
  • ANNOTATION_TYPE: 제약 조건 어노테이션을 기반으로 구성된 제약 조건을 만들 수 있음.

* 클래스 수준 제약 조건을 만들 때 요소 타입 TYPE을 사용해야 한다.

  생성자의 반환 값을 대상으로 하는 제약 조건은 요소 타입 CONSTRUCTOR를 지원해야 한다.

  메서드 또는 생성자의 모든 매개 변수를 함께 유효성 검사하는데 사용되는 교차 매개 변수 제약 조건은

  각각 METHOD 또는 CONSTRUCTOR를 지원해야 한다.

@Retention(RUNTIME)

리플렉션을 통해 런타임에 사용 가능하도록 지정한다.

@Constraint(validatedBy = CheckCaseValidator.class)

이 제약 조건 어노테이션이 달린 요소의 유효성을 검사하는 데 사용할 유효성 검사기를 지정한다.

@Documented

이 제약 조건 어노테이션이 달린 요소의 JavaDoc에 포함된다.

@Repeatable(List.class)

일반적으로 구성이 다른 동일한 위치에서 제약 조건 어노테이션이 여러 번 반복될 수 있음을 나타낸다.
제약 조건 어노테이션의 유형이 포함하고 있는 List

 

이 List는 동일한 요소에 여러개의 (다른 유효성 검사 그룹 및 메시지를 가진) 해당 제약 조건 어노테이션을 지정할 수 있습니다. 다른 이름을 사용할 수 있지만 Jakarta Bean Validation specification에서는 List라는 이름을 사용하고 어노테이션을 해당 제약 조건 타입의 내부 어노테이션으로 만들 것을 권장한다.

 

groups

groups를 사용하면 유효성 검사 중에 적용되는 제약 조건 집합을 제한할 수 있다.

유효성 검사 그룹의 한 가지 사용 사례는 각 단계에서 지정된 제약 조건 하위 집합만 유효성을 검사해야 하는 UI 마법사이다. 대상 그룹은 적절한 유효성 검사 메서드에 var-arg 매개 변수로 전달된다.

Step 2. validator 구현하기 - Implementing a constraint validator for the constraint

어노테이션을 정의했으면 정의한 어노테이션으로 요소의 유효성 검사를 할 수 있는 제약 조건 유효성 검사기를 만들어야 한다. 이를 위해 Jakarta Bean Validation 인터페이스 ConstraintValidator를 구현한다.

 

ConstraintValidator 인터페이스는 구현에서 설정되는 두 가지 타입 매개변수를 정의한다.

첫 번째 매개변수: 유효성을 검사할 어노테이션 타입

두 번째 매개변수: 유효성 검사기가 처리할 수 있는 요소의 타입

 

제한 조건이 여러 데이터 타입을 지원하는 경우 허용된 타입 별로 ConstraintValidator를 구현하고 Constraint Annotation에 등록해야 한다.

유효성 검사기의 구현

initialize() 메서드: 검증된 제약 조건의 속성 값에 대한 액세스를 제공하고 유효성 검사기의 필드에 저장할 수 있도록 한다.

isValid() 메서드: 실제 유효성 검사 로직이 포함되어 있다.                         

                          @CheckCase의 경우 이는 initialize()에서 검색된 대소문자 모드에 따라
                          주어진 문자열이 완전히 소문자인지 대문자인지 확인한다.
                          Jakarta Bean 유효성 검사 specification에서는 null 값을 유효한 것으로 간주할 것을 권장한다.

null이 요소에 유효한 값이 아닌 경우 명시적으로 @NotNull로 주석을 달아야 한다.

public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> {

    private CaseMode caseMode;

    @Override
    public void initialize(CheckCase constraintAnnotation) {
        this.caseMode = constraintAnnotation.value();
    }

    @Override
    public boolean isValid(String object, ConstraintValidatorContext constraintContext) {
        if ( object == null ) {
            return true;
        }

        if ( caseMode == CaseMode.UPPER ) {
            return object.equals( object.toUpperCase() );
        }
        else {
            return object.equals( object.toLowerCase() );
        }
    }
}

ConstraintValidatorContext - Using ConstraintValidatorContext to define custom error messages

public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> {

    private CaseMode caseMode;

    @Override
    public void initialize(CheckCase constraintAnnotation) {
        this.caseMode = constraintAnnotation.value();
    }

    @Override
    public boolean isValid(String object, ConstraintValidatorContext constraintContext) {
        if ( object == null ) {
            return true;
        }

        boolean isValid;
        if ( caseMode == CaseMode.UPPER ) {
            isValid = object.equals( object.toUpperCase() );
        }
        else {
            isValid = object.equals( object.toLowerCase() );
        }

        if ( !isValid ) {
            constraintContext.disableDefaultConstraintViolation();
            constraintContext.buildConstraintViolationWithTemplate(
                    "{org.hibernate.validator.referenceguide.chapter06." +
                    "constraintvalidatorcontext.CheckCase.message}"
            )
            .addConstraintViolation();
        }

        return isValid;
    }
}

HibernateConstraintValidator - ConstraintValidator 컨트렉에 대한 확장

목적

현재 ConstraintValidator 컨트렉에서 어노테이션만 매개 변수로 전달되므로 initialize() 메서드에 더 많은 컨텍스트 정보를 제공하는 것

Step 3. default 에러 메시지 정의하기 - Define a custom error message for the constraint.

default 에러 메시지를 정의하려면 다음 내용으로 ValidationMessages.properties 파일을 생성한다

org.hibernate.validator.referenceguide.chapter06.CheckCase.message=Case mode must be {value}.

유효성 검사 오류가 발생하면 유효성 검사 런타임은 이 리소스 번들에서 오류 메시지를 조회하기 위해 제약 조건 어노테이션의 message 속성에 지정한 기본값을 사용한다.

 

PRACTICE

 

참고

JSR 303 (Bean Validation 1.0) 명세

JSR 380 (Bean Validation 2.0) 명세

'개발 > 일지' 카테고리의 다른 글

[Error] Error executing DDL  (0) 2023.01.24
Big O  (0) 2023.01.23
[Error] NoClassDefFoundError: javax/xml/bind/JAXBException  (0) 2023.01.18
Java Bean Validation  (0) 2023.01.11
JPA Entity 작성 시 - @Setter 사용 X  (0) 2022.12.26