안녕하세요 오늘은 ConstraintValidator를 이용한 Validation 방식에 대해 알아보겠습니다. 이번 벨리데이션 방식의 경우 어노테이션을 이용하여 검증이 필요한 값에 설정한 후 어노테이션이 참조하는 커스텀한 ConstraintValidator를 이용하여 로직을 검증하고 이를 @Valid 어노테이션을 통해 검증하는 방식으로 진행합니다.
ConstraintValidator에 대해 알아보기 위해 Validator 동작 순서에 대해 알아보겠습니다. 가장 먼저 @Valid를 이용하기 위해 LocalValidatorFactoryBean 클래스가 필요합니다. LocalValidatorFactoryBean 클래스는 Spring에서 데이터 유효성 검증을 위한 클래스이며 @Valid 어노테이션을 검증을 원하는 변수에 붙이는 방식으로 사용하면 Spring에서 해당 변수의 유효성을 검증하여 결과를 출력합니다.
아래 코드는 LocalValidatorFactoryBean 클래스 중 일부입니다. LocalValidatorFactoryBean의 경우 ValidationFactory를 부팅하여 Validator 및 ValidatorFactory 인터페이스를 이용하여 데이터를 바인딩합니다.
public class LocalValidatorFactoryBean extends SpringValidatorAdapter
implements ValidatorFactory, ApplicationContextAware, InitializingBean, DisposableBean {
...
...
/**
* Specify a custom ConstraintValidatorFactory to use for this ValidatorFactory.
* <p>Default is a {@link SpringConstraintValidatorFactory}, delegating to the
* containing ApplicationContext for creating autowired ConstraintValidator instances.
*/
public void setConstraintValidatorFactory(ConstraintValidatorFactory constraintValidatorFactory) {
this.constraintValidatorFactory = constraintValidatorFactory;
}
...
...
ConstraintValidatorFactory targetConstraintValidatorFactory = this.constraintValidatorFactory;
if (targetConstraintValidatorFactory == null && this.applicationContext != null) {
targetConstraintValidatorFactory =
new SpringConstraintValidatorFactory(this.applicationContext.getAutowireCapableBeanFactory());
}
if (targetConstraintValidatorFactory != null) {
configuration.constraintValidatorFactory(targetConstraintValidatorFactory);
}
...
LocalValidatorFactoryBean 클래스를 사용하기 위해 수동으로 빈을 주입해줍니다. 만약 이 과정이 생략된다면 ConstraintValidatorFactory가 초기화되지 않아 검증을 진행할 targetValidator가 없어 오류가 발생합니다.
@Configuration
public class ValidationConfig {
@Bean
public LocalValidatorFactoryBean validatorFactoryBean() {
return new LocalValidatorFactoryBean();
}
}
ConstraintValidatorFactory 클래스의 경우 팩토리 메서드 패턴으로 ConstraintValidator를 생성하는 객체로 팩토리 메서드 패턴의 특성상 하위 클래스와의 결합도가 낮아 자유롭게 커스텀하여 적용할 수 있습니다. 이렇게 커스텀한 ConstraintValidator 클래스를 생성한 어노테이션과 연결한 후 검증을 원하는 변수에 생성한 어노테이션을 추가합니다.
아래 예시는 이메일을 검증하는 코드입니다. 여기서 주의할 점은 String.matches 대신 Pattern.matcher를 이용하는 것입니다. String.matches의 경우 내부에서 생성되는 Pattern 인스턴스가 한 번 쓰고 버려져 가비지 컬렉터의 대상이 되어 메모리 누수가 발생합니다. 따라서 Pattern 인스턴스를 직접 static으로 만들어 객체를 재사용할 수 있도록 하여 성능을 향상시킵니다.
@RequiredArgsConstructor
public class EmailValidator implements ConstraintValidator<ValidEmail,String> {
private static final Pattern EMAIL = Pattern.compile("^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*.[a-zA-Z]{2,3}$");
@Override
public void initialize(ValidEmail constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return EMAIL.matcher(value).matches();
}
}
아래는 새롭게 생성한 어노테이션입니다. @Constraint(validatedBy = EmailValidator.class)를 통해 현재 어노테이션이 붙어 있는 변수를 EmailValidator에서 처리하도록 합니다. 이 때 어노테이션 내부에 message, groups, payload 값은 LocalValidatorFactoryBean이 상속하는 SpringValidatorAdapter에서 검증하는 과정에서 필요한 데이터이기 때문에 비어있는 값으로라도 설정을 해주어야 합니다.
@Documented
@Constraint(validatedBy = EmailValidator.class)
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidEmail {
String message() default "Valid Email";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
이후 검증을 원하는 클래스 내부의 변수에 어노테이션을 추가합니다.
@ValidEmail
public String email;
이후 컨트롤러의 리퀘스트로 넘어오는 클래스 앞에 @Valid 어노테이션을 추가하면 빈을 주입받은 LocalValidatorFactoryBean이 @Valid 어노테이션을 통해 클래스 내부의 어노테이션을 체크합니다. 생성된 어노테이션에서는 생성한 커스텀 ConstraintValidator에서 로직을 처리해 결과를 반환하면 통과 여부에 따라 BindingResult에 결과가 저장되어 이 결과값을 토대로 검증을 진행할 수 있습니다.
public HashMap<String, Object> validateEmail(
@RequestBody @Valid ValidateEmailDTO validateEmailDTO,
BindingResult bindingResult)
{
if(bindingResult.hasErrors()) throw new ValidationException(404,"잘못된 요청값입니다.");