Spring/Spring Core

[Spring] Validation 추상화

TheWing 2020. 11. 4. 20:42

Validation 추상화

  • Spring에서 org.springframework.validation.Validator는 애플리케이션에서 사용하는 객체 검증용 인터페이스이다.
  • 주로 Spring MVC에서 사용하지만 Web 계층에서만 사용하는 전용 개념은 아니다.

특징

  • 어떠한 Layer이여도 상관이 없고 모든 Layer(Web, Service , Data) 에서 사용해도 상관없다.
  • 구현체 중 하나이면서 JSR-303(Bean Validation 1.0)과 JSR-349(Bean Validation 1.1), JSR-380(BeanValidation 2.0.1 )을 지원한다. (LocalValidatorFactoryBean)
  • DataBinder에 들어가서 Binding할 때 같이 사용되기도 한다.

주로 사용하는 @Annotation

 

 

 

인터페이스

  • boolean supports(Class c)
    • 어떤 타입의 객체를 검증할 때 사용할 것인지 결정한다
  • void validate(Object obj, Errors e)
    • 실제 검증 로직을 이 안에서 구현한다
      • 구현할 때 ValidationUtils 사용하며 구현하는 것이 편리하다
@Override
public boolean supports(Class<?> c) {
    return false; // 인스턴스가 검증 대상 타입인지 체크
}

@Override
public void validate(Object target, Errors errors) {
        // 검증 작업
}

구현 예제

  • Event 클래스 생성
public class Event {
    Integer id;

    String title;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }
}
  • Event 인스턴스에서 title 필드가 NotNull일때 가정
  • Event에 대한 Validation을 처리하는 EventValidator 클래스를 Validator 구현하여 생성한다
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;

public class EventValidator implements Validator {

    @Override
    public boolean supports(Class<?> c) {
        return Event.class.equals(c);
    }

    @Override
    public void validate(Object target, Errors errors) { //1
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "title","notEmpty","title이 비어있습니다."); //2
    }
}
  1. validate 메소드를 생성
  2. ValidationUtilsrejectIfEmptyOrWhitespace() 메소드를 사용하고 매개 변수에 Errors 객체, 필드명 , 에러코드, 메세지 기본값 을 넣는다
  3. 아래에 3번째 매개 변수인 errorCode는 key값에 해당하는 인터페이스를 가져오는 역할을 한다. 실제 메세지를 가져올 수 있는 코드이다.
  4. 마지막 매개변수인 defalutMessage는 에러코드를 찾이 못했을 때의 메세지를 넣어준다

AppRunner 구현

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.Errors;

import java.util.Arrays;

@Component
public class AppRunner implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) throws Exception {
        // Event 객체 생성하고 에러를 고의적으로 내기 위해 값을 넣지 않는다
        Event event = new Event();

        // EventValidator 객체를 생성
        EventValidator eventValidator = new EventValidator();

        // 검증할 Event 객체를 전달하여 Errors 인스턴스를 생성한다
        // BeanPropertyBindingResult 기본 구현체로 사용한다
        // Spring MVC에서 자동으로 생성해주고 실질적으로 직접 사용하지는 않는다.
        Errors errors = new BeanPropertyBindingResult(event, "event");

        // event 객체를 검사한다
        eventValidator.validate(event, errors);

        // errors에 error 가 있는지 확인한다
        System.out.println(errors.hasErrors());
        // forEach로 에러코드와 기본 메세지 출력
        errors.getAllErrors().forEach( e-> {
            System.out.println("----error code-----");
            Arrays.stream(e.getCodes()).forEach(System.out::println);
            System.out.println("e.getDefaultMessage() = " + e.getDefaultMessage());
        });
    }
}
  • 검증할 Event 객체 생성하고 에러를 고의적으로 내기 위해 값을 넣지 않는다
  • EventValidator 객체를 생성 후 검증할 Event 객체를 전달하여 Errors 인스턴스를 생성한다.
  • Errors 인스턴스를 생성할 때 구현체인 BeanPropertyBindingResult는 Spring MVC에서 자동으로 생성해주고 실질적으로 직접 사용하지는 않는다.
  • event 객체를 검사후 errors 객체에 error가 있는지 확인한다.
  • forEach문으로 에러코드와 기본 메세지를 출력한다.
  • 결과

 

errors.hasErrors() = true
----error code-----
notEmpty.event.title
notEmpty.title
notEmpty.java.lang.String
notEmpty
e.getDefaultMessage() = title이 비어있습니다.
  • 에러코드를 자동으로 생성해주므로 Validator 구현 시 에러코드는 prefix만 설정해주면 된다.
  • 이 방법은 가장 원시적인 예제입니다.
  • ValidationUtils 만 사용하는 것은 아니다.
  • 아래 예제를 확인해보자
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

public class EventValidator implements Validator {

    @Override
    public boolean supports(Class<?> c) {
        return Event.class.equals(c);
    }

    @Override
    public void validate(Object target, Errors errors) {
        // Generic 을 지원하지 않기 때문에 형변환을 직접 해줘야한다
        Event event = (Event)target;
        if (event.getTitle() == null) {
            // 여러 필드를 종합해서 봤을때 reject()
            // 특정 필드에 관해서 rejectValue()
            errors.rejectValue("title","notEmpty","title이 비어있습니다.");
        }
    }
}
errors.hasErrors() = true
----error code-----
notEmpty.event.title
notEmpty.title
notEmpty.java.lang.String
notEmpty
e.getDefaultMessage() = title이 비어있습니다.

Spring Boot 2.0.5 버전 이상 사용할 때

  • LocalValidatorFactoryBean 빈으로 자동으로 등록이 된다
  • JSR-380(Bean Validation 2.0.1) 구현체로 hibernate-validator를 사용한다

Spring 5에서 Bean Validation 2.0을 사용하려면

  • 아래를 의존으로 추가해준다
<!-- validation-api는 생략 가능 -->
<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>

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

LocalValidatorFactoryBean

  • Spring이 제공해주는 LocalValidatorFactoryBean 을 자동으로 빈 등록을 해준다
    Bean Annotation을 아무런 빈을 등록 안했지만 자동으로 빈 등록이 된다
  • AppRunner에서 LocalValidatorFactoryBean 으로 주입해도 무방하다.
@Autowired
Validator validator;

@Autowired
LocalValidatorFactoryBean validator1;
  • 검증하려면 Bean Validation 애노테이션을 붙여준다
import javax.validation.constraints.Email;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;

public class Event {
    Integer id;

    @NotEmpty
    String title;

    @NotNull @Min(0)
    Integer limit;

    @Email
    String email;

    public String getTitle() {
        return title;
    }

    public void setLimit(Integer limit) {
        this.limit = limit;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}
  • @NotEmpty
    • 빈 값 여부
  • @Min
    • 최소값
  • @Email
    • 이메일 주소 형식
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

import java.util.Arrays;

@Component
public class AppRunner implements ApplicationRunner {


    @Autowired
    Validator validator; 

    @Autowired 
    LocalValidatorFactoryBean validator1;  // 이것으로 사용해도 됩니다

    @Override
    public void run(ApplicationArguments args) throws Exception {
        //어떤 validator가 출력되는지 실행
        System.out.println(validator.getClass());

                //고의적으로 에러를 내기 위한 set
        Event event = new Event();
        event.setLimit(-1);
        event.setEmail("asdf");

        // 검증할 Event 객체를 전달하여 Errors 인스턴스를 생성한다
        Errors errors = new BeanPropertyBindingResult(event, "event");

        validator.validate(event, errors);

        // errors에 error 가 있는지 확인한다
        System.out.println("errors.hasErrors() = "+errors.hasErrors());
        // forEach로 에러코드와 기본 메세지 출력
        errors.getAllErrors().forEach( e-> {
            System.out.println("----error code-----");
            Arrays.stream(e.getCodes()).forEach(System.out::println);
            System.out.println("e.getDefaultMessage() = " + e.getDefaultMessage());
        });
    }
}
  • 결과
class org.springframework.validation.beanvalidation.LocalValidatorFactoryBean
errors.hasErrors() = true
----error code-----
Email.event.email
Email.email
Email.java.lang.String
Email
e.getDefaultMessage() = 이메일 주소가 유효하지 않습니다.
----error code-----
Min.event.limit
Min.limit
Min.java.lang.Integer
Min
e.getDefaultMessage() = 반드시 0보다 같거나 커야 합니다.
----error code-----
NotEmpty.event.title
NotEmpty.title
NotEmpty.java.lang.String
NotEmpty
e.getDefaultMessage() = 반드시 값이 존재하고 길이 혹은 크기가 0보다 커야 합니다.
  • 예상대로 title, limit, email 순으로 3개의 필드에서 에러가 발생했다.
    각 에러가 LocalValidatorFactoryBean 이 제공하는 에러코드와 메세지 기본값을 출력해준다

결론

  • Bean Validation 애노테이션으로 검증할 수 있는 것들을 LocalValidatorFactoryBean 을 주입하여 충분히 validation 처리가 가능해진다.
  • 상황에 따라서 validation을 해야하는지 Validator를 구현해야 하는지 상황에 맞게 사용하면된다

References