Whiteship's Note


Spring MVC Validation Testing



참조 :
Spring MVC 9장
springmodules의 valang을 활용한 Validation 테스트
Where is ErrorsVerifier class mentioned in Expert Spring MVC?

본문에서 Validator를 테스트 하는 방법을 소개하고 있습니다.
public class PersonValidatorTests extends TestCase {
    public void testEmptyPersonValidation() {
        Person person = new Person();
        Validator validator = new PersonValidator();
        BindException errors = new BindException(person, "target");
        validator.validate(person, errors);
        new ErrorsVerifier(errors) {
            {
            forProperty("firstName").hasErrorCode("person.firstName.required")
            .forProperty("lastName").hasErrorCode("person.lastName.required")
            .otherwise().noErrors();
            }
        }
    }
}

위의 소스 코드는 Spring MVC 281페이지 그대로 입니다. 하지만 위의 코드는 선뜻 이해가 가지 않았습니다.
언뜻 봤을 때 person 객체를 두번이나 사용한 것부터 뭔가 이상하게 느껴졌으며(좀 더 보고 있으니까 바인딩 할 때 한번 검증할 때 한 번 사용하는 의도 인것 같습니다.)
ErrorsVerifier라는 익명 클레스의 사용이 낯설었습니다.

ErrorsVerifier를 사용한 것을 보면 fluent interface 개념으로 구현하여 매우 재밌고 직관적으로 작명된 메소드의 이름들을 확인할 수 있습니다. 하지만 안타깝게도 책에 나온 설명과 달리 저 클레스(?)는 spring 프레임웤에 존재하지 않습니다.

이 클레스를 찾느라 여러명이 고생한 흔적을 구글에서 찾을 수 있었는데 그 곳에서 보다 더 값진 것을 발견했습니다. 바로 뛰어난 선배님재미난 참여를 살펴 볼 수 있었습니다.

위 링크에 가시면 Spring에는 전혀 없던 ErrorsVerifier 코드를 만들어 낸 과정이 들어있습니다.

ErrorsValidator 다운

ErrorsValidator를 사용한 예제 코드
public class MemberInfoValidatorTest extends TestCase{

    public void testEmptyMemberInfo() {
        MemberInfo memberInfo = new MemberInfo();
        Validator validator = new MemberInfoValidator();
        BindException errors = new BindException(memberInfo, "target");
        validator.validate(memberInfo, errors);
        ErrorsVerifier errorsVerifier = new ErrorsVerifier(errors);
        errorsVerifier.forProperty("name").hasErrorCode("required")
            .forProperty("password").hasErrorCode("required")
            .forProperty("confirmPassword").hasErrorCode("required")
            .forProperty("email").hasErrorCode("required");
    }
}

음~ 좋네요. 다시 보니까 Spring MVC 에 나온 코드는 컴파일 에러가 나지 않을까 싶네요;; 클레스파일이 없기도 없거니와 인너 클래스의 사용이 이상해요. 중괄호 연속 두 개씩.. 대체 무슨 의미인건지;;; 어떻게 하라는건지~ㅋㅋ

top


Spring MVC Validation 정리



Programmatic
- Vlidator - ValidationUtils 사용하기
- Vlidator - Property 파일 사용하기

Declarative
- Declarative Validators - Valang 사용하기
    - Valang - syntex
    - Valang - CustomFunction
    - ValangValidatorFactoryBean -> ValangValidator

참조
- Fail-fast vs. complete validation
- Really easy field validation 사용하기 (4)
- Spring MVC 9장
- Spring Modules


top


ValangValidatorFactoryBean -> ValangValidator



링크 : Spring Modules Wiki :: Using Valang validator

Spring Modules 사이트는 https 라 그런가 약간 느리고 답답합니다. Spring Modules API를 보던 중에 Spring MVC 9장에서 예제로 사용하는 ValangValidatorFactoryBean이 보이질 않았습니다. 이게 대체 무슨일인가... 하고 구글링을 했더니 Spring Modules 0.4 이전에 사용하던 클레스인데 0.5 부터는 ValangValidator로 대체 된 것(deprecated 된 후 삭제) 같습니다.
Prior to release 0.4, the org.springmodules.validation.ValangValidatorFactoryBean could be used as an alternative to org.springmodules.validation.ValangValidator. Since both classes served the same purpose, the former was deprecated and is planned to be removed in 0.5.
이름이 짧아져서 좋습니다. :)

둘의 차이점으로는 ValangValidatorFactoryBean에서는 syntax라는 property로 valang을 등록했는데 ValangValidator에서는 valang이라는 property에 설정한다는 것입니다. 보며 좋은 이름을 선택했다고 볼 수 있습니다.

그 외에 customFuction을 등록할 수 있는 것은 같습니다.

top


Valang - CustomFunction



Valang이 제공하는 여러 Validation로직이 있지만 사용자가 직접 특정 로직을 구현하여 끼워넣을 수 있습니다.

AbstractFunction 을 상속하여 구현합니다.

제약사항으로 세 개의 인자(Functions[] functions, int line, int column)를 가지는 생성자를 만들어서 AbstractFunction 클레스를 생성할 때 사용할 수 있도록 super를 호출해야 합니다.

Spring MVC 9장에 나와있는 예제로 그다지 쓸만해 보이지는 않지만 입력받은 값의 case를 뒤바꿔서 검증을 하는 예제가 나옵니다.

import org.apache.commons.lang.WordUtils;

import org.springmodules.validation.valang.functions.AbstractFunction;

import org.springmodules.validation.valang.functions.Function;

 

public class AlterCaseFunction extends AbstractFunction {

       public AlterCaseFunction(Function[] functions, int line, int column) {

             super(functions, line, column);

             definedMinNumberOfArguments(1);

             definedMaxNumberOfArguments(1);

       }

 

       protected Object doGetResult(Object target) {

             String value = getArguments()[0].getResult(target).toString();

             return WordUtils.swapCase(value);

       }

}

이렇게 작성한 Function을 ValangValidatorFactoryBean에 등록하여 syntax에서 사용할 수 있습니다.

<bean id="caseSwappingValidator"

       class="org.springmodules.validation.ValangValidatorFactoryBean">

       <property name="syntax">

             <value>

                    <![CDATA[

{ name : alterCase(?) = 'sTEVEN' : 'Name must be Steven' }

]]>

             </value>

       </property>

       <property name="customFunctions">

             <map>

                    <entry key="alterCase"

       value="com.apress.expertspringmvc.validation.AlterCaseFunction" />

             </map>

       </property>

</bean>

흠... 그런데..API에서 ValangValidatorFactoryBean 이녀석이 사라졌습니다. 어디로 갔을런지;;

top


Valang - syntex



기본 적인 syntex 예제 입니다.

{ location : ? is not blank : 'Location must be specified' : 'addressLocationEmpty' : ? }

1. validation syntex는 {와 } 사이에 들어가야 합니다.

2. 인자는 총 다섯 개가 올 수 있으며 : 로 구분합니다. 마지막 인자는 생략 가능합니다.
- 첫 번째 인자 : 검사할 객체의 프로퍼티
- 두 번째 인자 : 검사하는 로직
- 세 번째 인자 : default message
- 네 번째 인자 : 프로퍼티 파일의 에러코드
- 다섯 번째 인자 : 아규먼트

3. 프로퍼티를 가져오는 방법
- name
- address.location
- customers[0].name
- salesParameters[seasonStartDate]
위와 같이 다양한 벙법으로 가져 올 수 있으며 기본적으로 String 타입으로 가져옵니다. 이 때 []를 사용하면 그 안의 값은 Date 타입으로 인식합니다. 현재 시각을 나타내는 T 를 사용할 수 있습니다.
예) 입력한 시각이 현재 시각보다 앞서지 않도록 검증
{ order.timestamp : ? < [T] : '' : 'order.timestamp.in_past' }
예) 입력한 시각이 오늘이라는 시간대에 포함되는지 검증
{ order.timestamp : ? between [T<d] and [T>d] : '' : 'order.timestamp.not_today' : ? }

4. 로직을 작성할 때 사용할 수 있는 오퍼레이터들
- String 관련 오퍼레이터
사용자 삽입 이미지
사용자 삽입 이미지
- Date 관련 오퍼레이터
사용자 삽입 이미지
- Date Incremental operator
사용자 삽입 이미지
- Date Regural Expression
사용자 삽입 이미지
top

TAG Syntax, Valang

Declarative Validators - Valang 사용하기



XML 설정파일로 Validation을 할 수 있습니다.


Jakarta Commons Validator또는 Valang(Validation language)을 사용하여 선언적으로 Validation을 할 수 있습니다. 여기서는 SpringMVC 9장에서 소개하고 있는 Valang을 살펴봅니다.

1. 필요한 jar파일
- Spring Modules

2. 사용법
- org.springmodules.validation.ValangValidatorFactoryBean 사용하여 validation bean만들기
- syntax 등록
- custom function 등록
- Validation사용할 수 있는 Controller에 Setter Injection 시키면 끝!

3. 예제코드

<bean id="caseSwappingValidator"

       class="org.springmodules.validation.ValangValidatorFactoryBean">

       <property name="syntax">

             <value>

                    <![CDATA[

{ name : alterCase(?) = 'sTEVEN' : 'Name must be Steven' }

]]>

             </value>

       </property>

       <property name="customFunctions">

             <map>

                    <entry key="alterCase"

       value="com.apress.expertspringmvc.validation.AlterCaseFunction" />

             </map>

       </property>

</bean>


top


Vlidator - Property 파일 사용하기



이전 글에서 사용한 방법으로는 입력 필드에 값이 비어있는지만 확인할 수 있습니다. 아마도 Errors 객체를 사용할 때 값이 비어있는지 검사하려면 if 문이 자주 사용되니까 그 코드를 줄여주기 위해 만든것 같습니다.

따라서 빈 값인지 확인할 때는 유용하게 사용할 수 있지만 그 이외의 경우에는 Errors 인터페이스를 사용해야 한다.
사용자 삽입 이미지
Errors의 인터페이스 중에 에러 메시지를 기록할 수 있는 메소드들입니다.

인자의 종류를 살펴보면 다음과 같습니다.
field :: 에러 메시지를 남길 대상이 되는 필드
errorCode :: 프로퍼티 파일에 있는 에러 메시지의 이름(키)
errorAgs :: 프로퍼티 파일에 있는 에러 메시지의 내용(값)을 출력할 때 특정 문자열을 넘겨 줄 수 있습니다. 그 때 이 아규먼트를 사용합니다.
defaultMessage :: 프로퍼티 파일에서 errorCode에 해당하는 메시지 키를 못찾으면 여기에 입력한 값을 출력합니다.


프로퍼티 파일을 사용하는 방법은 간단합니다.
    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename" value="message" />
    </bean>

위와 같이 messageSource를 등록합니다. 이 때 프로퍼티 파일의 이름을 basename에 입력해 줍니다. 만약 프로퍼티 파일이 여러개라면 besenames 속성을 사용하여 다음과 같이 list로 넘겨줍니다.

<beans>
  <bean id="messageSource"
        class="org.springframework.context.support.ResourceBundleMessageSource">
    <property name="basenames">
      <list>
        <value>format</value>
        <value>exceptions</value>
        <value>windows</value>
      </list>
    </property>
  </bean>
</beans>

그 다음 프로퍼티 파일을 작성합니다. 위 설정에서 message 라는 이름을 입력했으니까 프로퍼티 파일은 message.properties 또는 message_ko_KR.properties 이런식으로 입력값 뒤에 지역코드가 붙은 프로퍼티 파일도 자동으로 읽히게 됩니다.

required=Input {0} Properties
passwordTooShort=Enter password at least 6 characters.
notSamePassword=Enter the same vlaue with password


프로퍼티에 저렇게 내용을 채우고 이제는 Validator를 만듭니다. 저번에 만든 코드에서 살짝 내용을 추가/수정했습니다.

public class MemberInfoValidator implements Validator{

    public boolean supports(Class clazz) {
        return MemberInfo.class.isAssignableFrom(clazz);
    }

    public void validate(Object object, Errors errors) {
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "email", "required", new Object [] {"email"}, "Enter your email");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "password", "required", new Object [] {"password"}, "Enter your password");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "confirmPassword", "required", new Object [] {"confirmPassword"}, "Enter the same password for confirmation");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "confirmMember", "required", new Object [] {"confirmMember"}, "Enter ajn member code");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "required", new Object [] {"name"}, "Enter your name");

        MemberInfo memberInfo = (MemberInfo)object;
        if(memberInfo.getPassword().length() < 6)
            errors.rejectValue("password", "passwordTooShort");

        if(!memberInfo.getPassword().equals(memberInfo.getConfirmPassword()))
            errors.rejectValue("confirmPassword", "notSamePassword");
    }

}

이제 실행해보면 다음과 같이 에러메시지들이 출력됩니다.
사용자 삽입 이미지


top


Vlidator - ValidationUtils 사용하기



Spring에서 Validator를 구현하는 방법은 두 가지가 있습니다.
1. Programmatic
2. Declarative

그 중에서 첫 번째 Programmatic 방법을 사용하여 구현할 때 ValidationUtils를 사용하면 매우 간단하게 구현할 수 있습니다.
사용자 삽입 이미지
인터페이스 중에 인자가 네개인 녀석을 사용하여 defaultMessage를 주면 프로퍼티 파일을 만들지 않아도 메시지를 출력할 수 있습니다.

1. Validator 만들기
public class MemberInfoValidator implements Validator{

    public boolean supports(Class clazz) {
        return MemberInfo.class.isAssignableFrom(clazz);
    }

    public void validate(Object object, Errors errors) {
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "email", "required", "Enter your email");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "password", "required", "Enter your password");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "confirmPassword", "required", "Enter the same password for confirmation");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "confirmMember", "required", "Enter ajn member code");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "required", "Enter your name");
    }

}

Validator 인터페이스를 구현하고 ValidationUtils를 사용하여 간단하게 구현할 수 있습니다.

2. Controller에 등록하기
  public CreateMemberInfoController() {
        setCommandClass(MemberInfo.class);
        setCommandName("memberInfo");
        setFormView("createMemberInfo");
        setSuccessView("viewMemberList");
        setValidator(new MemberInfoValidator());
    }

setValidator 메소드를 사용하여 위에서 만든 Validator를 등록해 줍니다.

3. 화면에 보여주기
<form:errors path="속성 이름" />

이런식으로 속성 이름을 적어주면 그 이름에 해당하는 에러 메시지를 고자리에 출력해 줍니다.

사용자 삽입 이미지

<form:errors path="*" />

이렇게 써주면 저 태그가 들어간 위치에 모든 에러 메시지를 모아서 출력할 수 있습니다. 좋군요~
사용자 삽입 이미지

top