2010년 10월 13일 수요일

JSR 303 (Bean Validation)

Domain 모델은 데이터베이스에 저장되는 Entity로 사용될 뿐만 아니라 사용자로 부터 입력받는 폼, 컨트롤러, 서비스, DAO 등 다양한 서비스 계층에서 사용되어 진다.
이러한 Domain 객체를 사용하는 각 서비스 계층에서는 도메인 객체의 프로퍼티 값이 유효한지 체크하게 된다.
예를 들면 컨트롤러에서는 사용자 입력 폼에서 필수 입력 항목이라든지 포맷등을 체크하거나 DAO에서 저장하기 전에 필수값 체크, 자리수 체크 등을 해야 한다.
스프링에서도 org.springframework.validation.Validator 인터페이스를 이용한 validation 기능을 제공한다. Spring3.x 에서는 JSR-303 의 Bean Validation을 이용한 validation을 사용할 수 있다.
이번 포스트에서는 JSR-303(Bean validation) 기능을 이용하여 Validation 기능을 사용하는 방법을 알아보기로 하자.

maven의 pom.xml 파일에 아래와 같이 dependency를 추가해 주어야 한다.
...
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.4</version>
      <type>jar</type>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-validator</artifactId>
      <version>4.0.2.GA</version>
      <type>jar</type>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>1.6.1</version>
      <type>jar</type>
      <scope>compile</scope>
    </dependency>
  </dependencies>
...

사용자 등록을 위한 User 도메인 객체를 생각해보자.
userId, password, email, age를 포함하고 있다고 가정하자

package myproject.spring3.validator;

public class User {
  
  String userId;
  String password;
  String email;
  int age;
  
  public String getUserId() {
    return userId;
  }
  public void setUserId(String userId) {
    this.userId = userId;
  }
  public String getPassword() {
    return password;
  }
  public void setPassword(String password) {
    this.password = password;
  }
  public String getEmail() {
    return email;
  }
  public void setEmail(String email) {
    this.email = email;
  }
  public int getAge() {
    return age;
  }
  public void setAge(int age) {
    this.age = age;
  }

}


위의 도메인 객체에 validation 기능을 추가해 보도록 하겠다.
우선 사용자 가입 시를 생각해 보자.
userId, password, age(19세 이상체크 왜?)는 필수 입력사항이다.
email은 null을 허용하지만 값이 설정되면 email포맷에 적합해야 한다.

Bean Validation은 annotation을 이용하여 validation체크를 한다.

package myproject.spring3.uservalidation;

import javax.validation.constraints.Min;

import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.NotEmpty;

public class User {
  @NotEmpty
  String userId;
  @NotEmpty
  String password;
  @Email
  String email;
  @Min(value=19)
  int age;
  
  public String getUserId() {
    return userId;
  }
  public void setUserId(String userId) {
    this.userId = userId;
  }
  public String getPassword() {
    return password;
  }
  public void setPassword(String password) {
    this.password = password;
  }
  public String getEmail() {
    return email;
  }
  public void setEmail(String email) {
    this.email = email;
  }
  public int getAge() {
    return age;
  }
  public void setAge(int age) {
    this.age = age;
  }

}


위에서 설정한 annotation으로 validation을 체크하는 소스는 아래와 같다.

package myproject.spring3.uservalidation;

import java.util.Set;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;

import junit.framework.Assert;

import org.junit.Before;
import org.junit.Test;

public class UserValidationTest {
  Validator validator;
  
  @Before
  public void init() throws Exception {
    ValidatorFactory factory = 
      Validation.buildDefaultValidatorFactory();
    this.validator = factory.getValidator();
  }
  
  @Test
  public void testUserInsertValidation_OK() throws Exception {
    User user = new User();
    
    user.setUserId("userid");
    user.setPassword("password");
    user.setAge(29);
    
    Set<ConstraintViolation<User>> violations = 
      validator.validate(user);
    
    System.out.println("violations: " + violations);
    
    Assert.assertEquals(violations.size(), 0);
  }

  @Test
  public void testUserInsertValidation_Email_Error() throws Exception {
    User user = new User();
    
    user.setUserId("userid");
    user.setPassword("password");
    user.setEmail("invalid-email");
    user.setAge(29);
    
    Set<ConstraintViolation<User>> violations = 
      validator.validate(user);
    
    System.out.println("violations: " + violations);
    
    Assert.assertEquals(violations.size(), 1);
  }

  @Test
  public void testUserInsertValidation_Email_Age_Error() throws Exception {
    User user = new User();
    
    user.setUserId("userid");
    user.setPassword("password");
    user.setEmail("invalid-email");
    user.setAge(18);
    
    Set<ConstraintViolation<User>> violations = 
      validator.validate(user);
    
    System.out.println("violations: " + violations);
    
    Assert.assertEquals(violations.size(), 2);
  }

  @Test
  public void testUserInsertValidation_Email_Age_Password_Error() throws Exception {
    User user = new User();
    
    user.setUserId("userid");
    user.setPassword(null);
    user.setEmail("invalid-email");
    user.setAge(18);
    
    Set<ConstraintViolation<User>> violations = 
      validator.validate(user);
    
    System.out.println("violations: " + violations);
    
    Assert.assertEquals(violations.size(), 3);
  }
  
}


2개 이상의 Validator 사용하기


도메인 객체의 property에 validation 을 annotation으로 설정하는 경우 2개 이상의 다른 validation을 설정해야 하는 경우는 어떻게 처리가 가능한가?
예를 들면 사용자 등록시에는 userId, password, email(null 이 아닌 경우에 포맷체크), age(19세 이상)에 대한 validation 체크를 하고, 삭제시에는 userId와 password에 대한 validation 체크를 한다고 가정해보자. User 도메인 클래스는 하나 밖에 없는데, Validation은 2개 이상처리해야 하므로 별도의 처리가 필요하다. 바로 groups라는 annotation 속성을 이용하면 된다.
groups는 클래스 목록을 설정할 수 있는 속성이다. 이름만 식별할 수 있는 인터페이스(또는 클래스)를 만들어서 각 property의 validator의 groups 속성에 지정한다.
위의 예제에서 사용자 등록 validation을 UserCreateGroup이라고 지정하고 삭제 validaton을 UserDeleteGroup이라고 지정한다.
userId와 password는 등록과 삭제시 공통으로 체크해야 하는 항목이므로 groups={ UserCreateGroup.class, UserDeleteGroup.class}로 지정하고, email과 age는 등록 시에만 체크해야 하므로 groups={UserCreateGroup.class}로 지정하면 된다.
아래에 변경된 User 클래스다.

package myproject.spring3.uservalidation;

import javax.validation.constraints.Min;

import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.NotEmpty;

public class User {
  @NotEmpty(groups={UserCreateGroup.class, UserDeleteGroup.class})
  String userId;
  @NotEmpty(groups={UserCreateGroup.class, UserDeleteGroup.class})
  String password;
  @Email(groups={UserCreateGroup.class})
  String email;
  @Min(value=19, groups={UserCreateGroup.class})
  int age;
  
  public String getUserId() {
    return userId;
  }
  public void setUserId(String userId) {
    this.userId = userId;
  }
  public String getPassword() {
    return password;
  }
  public void setPassword(String password) {
    this.password = password;
  }
  public String getEmail() {
    return email;
  }
  public void setEmail(String email) {
    this.email = email;
  }
  public int getAge() {
    return age;
  }
  public void setAge(int age) {
    this.age = age;
  }

}


package myproject.spring3.uservalidation;

public interface UserCreateGroup {

}


package myproject.spring3.uservalidation;

public interface UserDeleteGroup {

}


위의 테스트 케이스에 사용자 등록 및 삭제에 대한 테스트 메서드를 아래와 같이 추가하면 테스트가 가능하다.

@Test
  public void testUserCreateValidation() throws Exception {
    User user = new User();
    
    user.setUserId("userid");
    user.setPassword("password");
    user.setEmail("valide-email@gmail.com");
    user.setAge(19);
    
    Set<ConstraintViolation<User>> violations = 
      validator.validate(user, UserCreateGroup.class);
    
    System.out.println("violations: " + violations);
    
    Assert.assertEquals(violations.size(), 0);
    
  }

  @Test
  public void testUserDeleteValidation() throws Exception {
    User user = new User();
    
    user.setUserId("userid");
    user.setPassword("password");
    user.setEmail(null);
    user.setAge(0);
    
    Set<ConstraintViolation<User>> violations = 
      validator.validate(user, UserDeleteGroup.class);
    
    System.out.println("violations: " + violations);
    
    Assert.assertEquals(violations.size(), 0);
    
  }

  @Test
  public void testUserCreateAndDeleteValidation() throws Exception {
    User user = new User();
    
    user.setUserId("userid");
    user.setPassword("password");
    user.setEmail("valid-email@gmail.com");
    user.setAge(20);
    
    Set<ConstraintViolation<User>> violations = 
      validator.validate(user, UserDeleteGroup.class, UserCreateGroup.class);
    
    System.out.println("violations: " + violations);
    
    Assert.assertEquals(violations.size(), 0);
    
  }

  @Test
  public void testUserWithoutGroup() throws Exception {
    User user = new User();
    
//    user.setUserId("userid");
//    user.setPassword("password");
//    user.setEmail("valid-email@gmail.com");
//    user.setAge(0); 

    Set<ConstraintViolation<User>> violations = 
      validator.validate(user);
    
    System.out.println("violations: " + violations);
    
    Assert.assertEquals(violations.size(), 0);
    
  }

validator.validate 메서드의 첫 번째 파라미터는 validation을 체크하고 싶은 도메인 객체를 넘긴다.
두번째 파라미터부터는 varargs를 이용하여 Group 클래스를 n개 넘길 수 있다. 위의 세번째 메서드가 Create와 Delete를 동시에 validation 체크하는 예제다. 이렇게 사용할 경우가 있을지는 좀 고민해볼 사항이다.
Validation annotation의 groups 속성을 지정하였을 경우, validator.validate에 Group 클래스를 지정하지 않으면 무조건 유효한 것으로 처리한다.