본문 바로가기

STUDY/스프링 철저 입문

3-3) 스프링 입력값 검사: Bean Validation, Hibernate Validator, @Valid, @Constraint, ConstraintValidator

 - 입력값 검사

 

웹서비스를 이용하다보면 다양한 입력창과 유형에 따른 유효성 검사를 볼 수 있다. 예를들어, 이메일 입력창이 있으면 이메일 양식에 맞는지 검사가 이루어진다. Front에서 유효성 검사로 Server 호출을 막지만, 세상에는 짖굳은 사람들이 많기에 Front 유효성 검사를 무력화 시키는 경우도 있다. 따라서, 서버에서도 동일하게 입력값 체크를 해야한다.

 

 

 - 스프링에서의 입력값 검사: Bean Validation, Hibernate Validator

 

스프링에서는 데이터 검사 표준인 Bean Validation를 사용한다. 더 정확히는 그 구현체인 Hibernate Validator를 사용한다. 먼저 아래와 같이 Hibernate Validator를 사용할 수 있도록 dependency를 추가한다.

<dependency>
  <groupId>org.hibernate.validator</groupId>
  <artifactId>hibernate-validator</artifactId>
  <version>6.0.18.Final</version>
</dependency>

 

 

@Data
public class NotNullInDto {
  @NotNull
  private String var;
}

 

@PostMapping(path = {"/notnull"})
public String notnull(@Valid @RequestBody NotNullInDto inDto) {
  return "SUCCESS";
}

 

 위와같이 @NotNull 유효성 검사가 적용된 NotNullInDto를 만들고 핸들러에 매개변수 타입으로 사용하였다. 그리고 해당 매개변수에 @Valid를 달면, 핸들러가 호출될 때 매개변수에 적용된 유효성검사를 수행한다. 위 /notnull API를 var=null 값으로 호출하면 methodArgumentNotValidException이 발생한다.

 

 

@PostMapping(path = {"/notnull"})
public Map<String, Object> notnull(@Valid @RequestBody NotNullInDto inDto, BindingResult bindingResult) {
  Map<String, Object> res = new HashMap<String, Object>();
  if(bindingResult.hasErrors()) {
    res.put("result", bindingResult.getAllErrors());
    res.put("response", inDto);
  }
  else {      
    res.put("result", "Ok");
    res.put("response", inDto);
  }
  return res;
}

 추가적으로 핸들러 매개변수에 BindingResult 타입을 넣으면, 유효성 검사 결과가 들어가고 예외를 발생시키지 않는다.

 

 

 - 검사 규칙

 

Hibernate Validator는 다양한 검사 규칙을 기본으로 제공한다.

  • @NotNull : null이 아닐 것
  • @min, @max: 허용하는 최소, 최대 값 검사
  • @Size: 문자열, 배열등의 길이 검사(최대 길이)
  • @Pattern: 정규 표현식으로 유효성 검사
  • 등등...

위의 예시는 일부에 불과하다. 추가적으로 모든 검사 규칙을 보고 싶으면 아래 사이트를 참고하면 된다.

docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#section-builtin-constraints

 

 

 - 커스텀 유효성 검사 규칙

 

기본으로 제공되는 다양한 검사 규칙이 있지만, 추가적으로 서비스에 맞는 추가규칙이 필요할 때가 있다. 예를 들어 핸드폰 번호를 필수값으로 입력받아야 한다면, @NotNull과 @Pattern를 조합하여 만들어야할 것이다. 하지만 이 두 규칙을 새로 유효성 검사 규칙으로 만들 수 있다.

 

@Documented
@Constraint(validatedBy = {})
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@ReportAsSingleViolation
@NotNull
@Pattern(regexp = "^[0-9]{3}-[0-9]{3,4}-[0-9]{4}$")
public @interface PhoneNumber {
  
  String message() default "PhoneNumber not valid";
  Class<?>[] groups() default {};
  Class<? extends Payload>[] payload() default {};
  
  @Target({ElementType.FIELD})
  @Retention(RetentionPolicy.RUNTIME)
  @Documented
  public @interface List{
    PhoneNumber[] value();
  }
}

 

 

 - 커스텀 유효성 검사기

 

지금까지 검사 규칙 어노테이션을 이용한 유효성 검사에 대해 알아봤다. 이 방식은 하나의 속성에 대한 유효성 검사는 할 수 있지만, 두 속성을 비교하는 식의 유효성 검사는 할 수 없다. 이때 필요한것이 커스텀 유효성 검사기, 새로운 ConstraintValidator 구현이다.

 

 

@Documented
@Constraint(validatedBy = {IsEqualTwoPropertiesValidator.class})
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface IsEqualTwoProperties {
  
  String message() default "Two properties not equal";
  Class<?>[] groups() default {};
  Class<? extends Payload>[] payload() default {};
  
  String property1();
  String property2();
  
  @Target({ElementType.TYPE})
  @Retention(RetentionPolicy.RUNTIME)
  @Documented
  public @interface List{
    IsEqualTwoProperties[] value();
  }
}

 

public class IsEqualTwoPropertiesValidator implements ConstraintValidator<IsEqualTwoProperties, Object> {
    
  private String property1;
  private String property2;
  private String message;
  
  @Override
  public void initialize(IsEqualTwoProperties constraintAnnotation) {
    // TODO Auto-generated method stub
    property1 = constraintAnnotation.property1();
    property2 = constraintAnnotation.property2();
    message = constraintAnnotation.message();
  }



  @Override
  public boolean isValid(Object value, ConstraintValidatorContext context) {
    // TODO Auto-generated method stub
    BeanWrapper beanWrapper = new BeanWrapperImpl(value);
    Object property1Value = beanWrapper.getPropertyValue(property1);
    Object property2Value = beanWrapper.getPropertyValue(property2);
    Boolean result = ObjectUtils.nullSafeEquals(property1Value, property2Value);
    
    if(!result) {
      context.disableDefaultConstraintViolation();
      context.buildConstraintViolationWithTemplate(message).addPropertyNode(property1).addConstraintViolation();
    }
    
    return result;
  }
}

 

 먼저 커스텀 유효성 검사기를 사용하는 규칙을 새로 만들었다. 그리고 두개의 속성을 가지고 있고, 이 두개의 속성을 검사기에서 비교한다. 검사기는 ConstraintValidator를 구현하여 만들어진다. initialize에서 유효성 규칙에 관한 정보를 받아오고, isValid에서 규칙이 적용된 객체의 값을 가져와서 유효성 검사를 수행한다.

728x90