비지니스 로직에서 유효성체크를 검토하는 것은 장,단점이 있고 Spring은 그것들중 하나도 배제하지 않는 유효성체크(그리고 데이터바인딩)을 위한 디자인을 제공한다. 특히 유효성체크는 웹계층과 연관이 없어야하고, 어떤 장소에 배치하기(localize) 쉬워야한다 그리고 어떤 유효성을 가능하게하는 사람이 플러그인 할 수 있게 해야한다. 위에서 말한것을 고려하면, Spring은 어플리케인션 모든 계층내에 기본적이고 사용할 수 있는것 사이의 Validator 인터페이스를 제안하고 있다(has come up with).
Data binding은 어플리케이션 도메인 모델(또는 당신이 사용자 입력 진행을 위해 사용하는 객체들 무엇이든지)에 다이나믹하게 묶인것을 허락된 사용자가 입력하기에 유용하다. Spring은 Data binding을 정확하게 하기위해 소위 DataBinder을 제공한다. Validator와 DataBinder는 validation 패키지를 구성하고, 처음에 사용되었던것 안에 구성되어있다. 그러나 MVC framework안에 제안되어있지는 않다.
BeanWrapper는 Spring 프레임워크에서 기본적인 개념이고, 많은곳에서 사용되어진다. 그러나, BeanWrapper를 직접 사용할 필요는 아마 결코 없을 것이다. 이것은 참고 문서이기 때문에, 우리는 적절한 몇몇 설명을 생각해 본다. 이번 장(chapter)에서는 BeanWrapper를 설명한다. 만약 여러분이 이것을 모두 사용한다면, 아마 BeanWrapper와 강하게 관련이 있는 객체인 bind data를 연습해 볼 수 있을것이다.
스프링은 모든 장소위에 PropertyEditor를 사용한다. PropertyEditor의 개념은 JavaBeans 명세(specification)의 일부분이다. BeanWrapper와 DataBinder가 밀접하게 연관된 이후로, BeanWrapper일때, 또한 이번 장(chapter)내에 PropertyEditors 사용을 잘 설명한다.
Spring의 기능인 Validator인터페이스는 당신이 객체의 유효성을 체크하기 위해 사용할수 있다. Validator인터페이스는 일관적이고 Errors객체라 불리는 것을 사용하여 작동한다. 반면에, 유효성을 체크하는 동안, validator는 Errors객체에 대한 유효성체크 실패를 보고할것이다.
작은 데이타 객체를 고려해보자.
public class Person {
private String name;
private int age;
// the usual getters and setters...
}
org.springframework.validation.Validator를 사용하여 우리는 Person클래스를 위한 유효성체크 행위를 제공할것이다.
supports(Class) - 이 Validator가 주어진 Class의 인스턴스를 체크하는가.?
validate(Object, org.springframework.validation.Errors) - 주어진 객체에 대한 유효성 체크를 수행하고 유효성 에러발생시 주어진 Errors객체를 가진 등록자를 둔다.
특히 Spring이 제공하는 ValidationUtils를 알고 있을때 Validator를 구현하는 것은 상당히 일관적이다.
public class PersonValidator implements Validator {
/**
* This Validator validates just Person instances
*/
public boolean supports(Class clazz) {
return Person.class.equals(clazz);
}
public void validate(Object obj, Errors e) {
ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
Person p = (Person) obj;
if (p.getAge() < 0) {
e.rejectValue("age", "negativevalue");
} else if (p.getAge() > 110) {
e.rejectValue("age", "too.darn.old");
}
}
}
당신이 볼수 있는것처럼, ValidationUtils클래스의 정적(static) rejectIfEmpty(..)메소드는 null이거나 공백일때 'name'프라퍼티를 제거하기 위해 사용된다. 이 예제가 제공하는 이외의 기능이 무엇인지 보기 위해 ValidationUtils를 위한 JavaDoc를 보라.
While it is certainly possible to implement a single Validator class to validate each of the nested objects in a rich object, it may be better to encapsulate the validation logic for each nested class of object in its own Validator implementation. A simple example of a 'rich' object would be a Customer that is composed of two String properties (a first and second name) and a complex Address object. Address objects may be used independant of Customer objects, and so a distinct AddressValidator has been implemented. If you want your CustomerValidator to reuse the logic contained within the AddressValidator class without recourse to copy-n-paste you can dependency-inject or instantiate an AddressValidator within your CustomerValidator, and use it like so:
public class CustomerValidator implements Validator {
private final Validator addressValidator;
public UserValidator(Validator addressValidator) {
if (addressValidator == null) {
throw new IllegalArgumentException("The supplied [Validator] is required and must not be null.");
}
if (!addressValidator.supports(Address.class)) {
throw new IllegalArgumentException(
"The supplied [Validator] must support the validation of [Address] instances.");
}
this.addressValidator = addressValidator;
}
/**
* This Validator validates Customer instances, and any subclasses of Customer too
*/
public boolean supports(Class clazz) {
return Customer.class.isAssignableFrom(clazz);
}
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.required");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required");
Customer customer = (Customer) target;
try {
errors.pushNestedPath("address");
ValidationUtils.invokeValidator(this.addressValidator, customer.getAddress(), errors);
} finally {
errors.popNestedPath();
}
}
}
유효성체크 에러는 validator에 전달되는 Errors객체를 보고한다. Spring Web MVC의 경우 당신은 error메시지를 점검하기 위해 spring:bind태그를 사용할수 있다. 하지만 물론 당신은 errors객체 자체를 점검할수 있다. 제공하는 메소드는 매우 일관적이다. 좀더 많은 정보는 JavaDoc에서 찾을수 있다.
우리는 데이터바인딩(databinding) 과 유효성체크(validation)에 대해 말했다. 유효성체크 에러에 대응하는 메시지를 출력하는 것은 우리가 언급할 필요가 있는 마지막 사항이다. 위에서 보여준 예제에서, 우리는 name 그리고 age필드를 제거한다. 만약 MessageSource를 사용한다면, 우리는 error메시지를 출력할것이다. 필드를 제거(이 경우 'name'과 'age')할때 주어진 error코드를 사용할것이다. 당신이 rejectValue나 Errors인터페이스로부터 다른 reject메소드중 하나를 호출(직접적이거나 직접적이지 않거나 ValidationUtils클래스 예제를 사용하여)할때, 근본적인 구현물은 당신이 전달한 코드뿐 아니라 많은 수의 추가적인 error코드를 등록할것이다. 등록한 error코드는 사용한 MessageCodesResolver에 의해 판단된다. 디폴트로 DefaultMessageCodesResolver가 사용되어, 당신이 준 코드를 가진 메시지를 등록할 뿐 아니라 reject메소드에 전달하는 필드명을 포함하는 메시지의 예제를 위해 DefaultMessageCodesResolver가 사용된다. too.darn.old코드 이외에 rejectValue("age", "too.darn.old")를 사용하여 필드를 제거하는 경우에, Spring은 too.darn.old.age 과 too.darn.old.age.int를 등록할것이다(첫번째는 필드명을 포함하고 두번째는 필드의 타입을 포함할것이다.).
MessageCodesResolver와 디폴트 전략에 대한 좀더 많은 정보가 각각 MessageCodesResolver와 DefaultMessageCodesResolver를 위한 JavaDoc에서 찾을수 있다.
org.springframework.beans 패키지는 Sun에서 제공하는 JavaBeans 표준을 고수한다(adhere). JavaBean은 인수가 없는 디폴트 구성자로된 간단한 클래스이고, bingoMadness이란 이름의 속성(property)은 setter setBingoMadness(..) 과 getter getBingoMadness()을 가진 네이밍 규칙을 따른다. JavaBeans과 명세서에 관한 더 많은 정보를 위해서, Sun의 웹사이트(java.sun.com/products/javabeans)를 방문하기 바란다.
beans 패키지의 아주 중요한 한가지는 BeanWrapper 인터페이스이고 인터페이스에 대응하는 구현(BeanWrapperImpl)이다. JavaDoc의 주석과 같이 BeanWrapper는 기능적인 set과 get 속성값들(개별적인 하나하나 또는 대량)을 제공하고, 속성 서술자들(descriptors)을 얻고, 읽거나 쓸수있는지를 결정하는 속성들을 질의한다(query). 또한, BeanWrapper는 내포된 속성들을 위한 지원을 제공하고, 제한된 깊이의 하위-속성내의 속성을 설정 가능하게 해준다. 그 다음, BeanWrapper은 target 클래스안에 지원 코드의 필요 없이 PropertyChangeListeners와 VetoableChangeListeners 표준 JavaBeans를 더하는 능력을 지원한다. 마지막으로, BeanWrapper는 인덱스 속성들을 설정하기위한 지원을 제공한다. BeanWrapper은 보통 직접적으로 애플리케이션 코드에 사용되지 않는다. 하지만 DataBinder과 BeanFactory에는 사용된다.
BeanWrapper를 작업하는 방법은 부분적으로 이름에의해 지시되어진다: 설정속성들과 검색한 속성들과 같이 it wraps a bean은 bean안에서 활동이 수행된다. (it wraps a bean to perform actions on that bean, like setting and retrieving properties.)
Setting과 getting 속성들은 setPropertyValue(s) 과 getPropertyValue(s) 메소드를 사용한다. (Setting and getting properties is done using the setPropertyValue(s) and getPropertyValue(s) methods that both come with a couple of overloaded variants.) Spring JavaDoc안에 더 자세한 모든것이 설명되어있다. 중요하게 알아야할것은 한객체가 지시하는 속성에 맞는 연결된(a couple of) 규약이다. 연결된 예들:
Table 5.1. Examples of properties
Expression | Explanation |
---|---|
name | name과 대응하는 속성은 getName() 또는 isName() 그리고 setName()를 나타낸다. |
account.name | account 속성의 내포된 name과 대응되는 것은 속성은 예를드면 getAccount().setName() 또는 getAccount().getName() 메소드들을 나타낸다. |
account[2] | account의 인덱스(Indexed) 속성, 세가지요소를 나타낸다. 인덱스 속성은 array의 형태, list 또는 있는 그대로의 순서화된 collection의 형태로 나타낼수 있다. |
account[COMPANYNAME] | account Map 속성의 COMPANYNAME 키는 색인한 map entry의 값을 나타낸다. |
get과 set 속성들을 BeanWrapper로 작업한 몇몇 예제들은 아래에 있다.
주의 : 이부분은 직접적으로 BeanWrapper를 작업할 계획이 없으면 중요하지 않다. 만약 DataBinder와 BeanFactory 그리고 아래 박스이외의(out-of-the-box) 구현을 사용한다면 PropertyEditors section으로 넘어가라.
다음 두 클래스들을 주시하라 :
public class Company { private String name; private Employee managingDirector; public String getName() { return this.name; } public void setName(String name) { this.name = name; } public Employee getManagingDirector() { return this.managingDirector; } public void setManagingDirector(Employee managingDirector) { this.managingDirector = managingDirector; } }
public class Employee { private float salary; public float getSalary() { return salary; } public void setSalary(float salary) { this.salary = salary; } }
다음 코드 조각들은 검색하는 방법과 속성들을 사례를 들어 증명하는 조작에 대한 예들이 있다: Companies 과 Employees
BeanWrapper company = BeanWrapperImpl(new Company()); // setting the company name.. company.setPropertyValue("name", "Some Company Inc."); // ... can also be done like this: PropertyValue value = new PropertyValue("name", "Some Company Inc."); company.setPropertyValue(value); // ok, let's create the director and tie it to the company: BeanWrapper jim = BeanWrapperImpl(new Employee()); jim.setPropertyValue("name", "Jim Stravinsky"); company.setPropertyValue("managingDirector", jim.getWrappedInstance()); // retrieving the salary of the managingDirector through the company Float salary = (Float) company.getPropertyValue("managingDirector.salary");
Spring은 PropertyEditors의 개념을 많이(heavily) 사용한다. 때때로 객체 자신보다 다른 방법으로 속성들을 알맞게 나타낼수 있을지도 모른다. 예를들어, 날짜는 사람이 읽을수 있는 방법으로 나타내야한다. 게다가 우리는 여전히 최초의 날짜를 거꾸로하여 사람이 읽을 수 있는 형태로 변환한다. (또는 더 나은것: Date objects의 뒤, 어떤 날짜를 사람이 읽을수 는 형태로 넣어 변환한다.) 이 행위자는 java.beans.PropertyEditor 형태의 등록한 사용자 에디터들(registering custom editors)에의해 목적을 이룰수 있다. BeanWrapper내 또는 3장에서 언급한것 같이 특정한 Application Context 안에 등록한 사용자 에디터들은 속성들이 원하는 형태로 변환하는 방법이 주어진다. Sun사에서 제공하는 java.beans 패키지의 JavaDoc문서에있는 더많은 PropertyEditors 정보를 읽어라.
Spring에서 사용하되는 편집속성 예
beans내의 설정 속성들은 PropertyEditors를 사용된다. XML 파일안에 선언한 몇몇 bean 속성값과 같이 java.lang.String을 언급할때, Spring은 (상응하는 setter가 Class-parameter를 가지고 있다면 ) Class 객체의 인수를 결정하기위해 ClassEditor를 사용한다.
Spring MVC framework내의 HTTP request parameters 분석에는 CommandController의 모든 하위클래스로 수동으로 바인드 할 수 있는 PropertyEditors종류가 사용된다.
Spring은 생명주기(life)를 쉽게 만들기 위한 내장(built-in) PropertyEditors를 가진다. 각각은 아래에 목록화되어있고, org.springframework.beans.propertyeditors 패키지 안에 모두 위치해 있다. 대부분, (아래에 나타나 있는것과 같이) 모두 그런것은 아니고 BeanWrapperImpl 디폴트로 저장되어있다. 속성 에디터가 몇몇 형태로 구성할수 있고, 디폴트 형태를 오버라이드하여 자신의 형상으로 등록하는 과정을 할 수 있다.
Table 5.2. 내장 PropertyEditors
Class | 설명 |
---|---|
ByteArrayPropertyEditor | byte 배열을 위한 Editor. Strings은 간단하게 byte 표현과 상응하여 변환될 것이다. BeanWrapperImpl에 의해 디폴트로 등록된다. |
ClassEditor | Strings으로 표현된 클래스들을 실제 클래스와 다른 주위의 방법으로 파싱하라. 클래스를 찾을수 없을때, IllegalArgumentException을 던진다. BeanWrapperImpl에 의해 디폴트로 등록된다. |
CustomBooleanEditor | Boolean속성들을 위해 커스터마이즈할수 있는 속성 에디터(editor). BeanWrapperImpl에 의해 디폴트로 등록된다.그러나, 사용자정의 편집기에 의해 등록된 custom 인스턴스에를 오버라이드할 수 있다. |
CustomCollectionEditor | Collection 형태의 목표가 주어졌을때 소스 Collection으로 전환하는, Collections을 위한 설정 editor. |
CustomDateEditor | 사용자정의 DateFormat을 지원한, java.util.Date을 위한 커스터마이즈할 수 있는 설정 editor. 디폴트에의해 등록할 수 없다. 사용자가 적절한 포맷을 소유함으로써 등록되어져야 한다. |
CustomNumberEditor | Integer, Long, Float, Double과 같은 Number 서브클래스를 위한 커스터마이즈할 수 있는 설정 에디터. BeanWrapperImpl에 의해 디폴트로 등록. 그러나, 사용자정의 편집기로 등록된 사용자정의 인스턴스를 오버라이드할 수 있다. |
FileEditor | Strings에서 java.io.File-객체들을 결정 가능. BeanWrapperImpl에 의해 디폴트로 등록된다. |
InputStreamEditor | 단방향 설정 에디터는 텍스트 문자열을 가질 수 있고, InputStream을 생성할 수 있다. (ResourceEditor와 Resource의 조정자를 통해서). 그래서 InputStream 프라퍼티들은 Strings으로 직접 셋팅될수도 있다. 디폴트 사용은 InputStream을 닫지 않는다는 것을 주의하라!. BeanWrapperImpl에 의해 디폴트로 등록된다. |
LocaleEditor | Strings에서 Locale-객체들을 결정 가능하고 반대로 Locale의 toString() 메소드도 같은 기능을 제공한다(String 포맷은 [language]_[country]_[variant]이다) . BeanWrapperImpl에 의해 디폴트로 등록된다. |
PropertiesEditor | Strings(Javadoc 안의 java.lang.Properties class에서 정의된 포맷되어 사용된 형태)을 Properties-객체들로 변환 가능하다. BeanWrapperImpl에 의해 디폴트로 등록된다. |
StringArrayPropertyEditor | String을 String-배열로 콤마-범위로 목록화하여 결정할 수 있고, 반대로도 가능하다. |
StringTrimmerEditor | Property 에디터는 Strings을 정리 정돈한다. 선택적으로 널(null) 값으로 되어있는 변형된 비어있는 문자열로 인정한다. 디폴트에의해 등록되어지지 않는다. 사용자는 필요할때에 등록해야한다. |
URLEditor | URL의 String 표현을 실제 URL-객체로 결정할 수 있다. BeanWrapperImpl에 의해 디폴트로 등록된다. |
Spring은 설정 에디터들이 필요할지도 모르는 것을 위하여 경로 찾기를 정하는 java.beans.PropertyEditorManager를 사용한다. 경로 찾기는 또한 Font, Color, 모든 원시 형태들의 PropertyEditors를 포함하고 있는 sun.bean.editor를 나타낸다. 만약 다루는 클개스가 같은 패키지 안에 있고, 클래스가 같은 이름을 가지고 있고, 'Editor'가 추가되어있다면, 또한 표준 JavaBeans 기초구조는 (등록하는 절차 없이) 자동적으로 PropertyEditor를 발견한다는 것을 주의하라. 예를 들어 Foo 타입의 프라퍼티를 위한 PropertyEditor로 인식되고 사용되기 위한 FooEditor 클래스를 위해 충분한 다음의 클래스와 패키지 구조를 가질수 있다.
com
chank
pop
Foo
FooEditor // the PropertyEditor for the Foo class
당신은 여기서 표준적인 BeanInfo JavaBean기법(여기서 놀랍지 않은 방법으로 언급된)을 사용할수 있다. 관련 클래스의 프라퍼티를 가진 하나 또는 그 이상의 PropertyEditor 인스턴스를 명시적으로 등록하기 위한 BeanInfo기법의 사용 예제를 아래에서 보라.
com
chank
pop
Foo
FooBeanInfo // the BeanInfo for the Foo class
그리고 이것은 참조된 FooBeanInfo 클래스를 위한 Java소스코드이다. 이것은 Foo 클래스의 age 프라퍼티를 가진 CustomNumberEditor에 관계될것이다.
public class FooBeanInfo extends SimpleBeanInfo { public PropertyDescriptor[] getPropertyDescriptors() { try { final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true); PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Foo.class) { public PropertyEditor createPropertyEditor(Object bean) { return numberPE; }; }; return new PropertyDescriptor[] { ageDescriptor }; } catch (IntrospectionException ex) { throw new Error(ex.toString()); } } }
string값으로 bean프라퍼티를 셋팅할때, Spring IoC컨테이너는 이러한 String을 프라퍼티의 복잡한 타입으로 변환하기 위해 표준 JavaBeans PropertyEditors를 사용한다. Spring은 (예를 들어, string으로 표현되는 classname을 실제 Class객체로 변환하기 위해)사용자정의 PropertyEditors의 수를 미리 등록한다. 추가적으로, Java의 표준 JavaBeans PropertyEditor는 적절하게 명명되고 자동적으로 발견하는 지원을 제공하는 클래스와 같은 패키지내 위치한 클래스를 위한 PropertyEditor를 허용하는 기법을 룩업한다.
다른 사용자정의 PropertyEditors를 등록할 필요가 있다면, 사용가능한 다양한 기법이 있다. 대개 편리하거나 추천되지 않는 대부분의 수동 접근법은 BeanFactory 참조를 가지는 것을 가정하는 ConfigurableBeanFactory 인터페이스의 registerCustomEditor()메소드를 간단히 사용한다. 좀더 편리한 기법은 CustomEditorConfigurer라고 불리는 특별한 bean factory 후-처리자를 사용한다. 비록 bean factory 후-처리자는 BeanFactory 구현물과 반-수동으로 사용될수 있다. 이것은 내포된 프라퍼티 셋업을 가진다. 그래서 다른 bean에 유사한 형태로 배치되고 자동으로 감지되고 적용되는 ApplicationContext와 사용되는 것이 강력하게 추천된다.
모든 bean factory와 애플리케이션 컨텍스트는 프라퍼티 변환을 다루기 위해 BeanWrapper라고 불리는 몇가지의 사용을 통해 많은 수의 내장된 프라퍼티 편집기를 자동으로 사용한다. BeanWrapper가 등록한 표준 프라퍼티 편집기는 다음 장에서 목록화된다. 추가적으로, ApplicationContexts는 애플리케이션 컨텍스트 타입을 명시하기 위해 적절한 방법으로 자원 룩업을 다루는 추가적인 수의 편집기를 오버라이드하거나 추가한다.
표준 JavaBeans PropertyEditor 인스턴스는 string으로 표현되는 프라퍼티 값을 프라퍼티의 복잡한 타입으로 변환하기 위해 사용된다. bean factory 후-처리자인 CustomEditorConfigurer는 ApplicationContext를 위한 추가적인 PropertyEditor 인스턴스를 위해 편리하게 추가된 지원을 위해 사용된다.
사용자 클래스인 ExoticType를 보자. 다른 클래스 DependsOnExoticType는 프라퍼티로 셋팅되는 ExoticType가 필요하다.
package example; public class ExoticType { private String name; public ExoticType(String name) { this.name = name; } } public class DependsOnExoticType { private ExoticType type; public void setType(ExoticType type) { this.type = type; } }
When things are properly set up, we want to be able to assign the type property as a string, which a PropertyEditor will behind the scenes convert into a real ExoticType object:
<bean id="sample" class="example.DependsOnExoticType"> <property name="type" value="aNameForExoticType"/> </bean>
The PropertyEditor implementation could look similar to this:
// converts string representation to ExoticType object
package example;
public class ExoticTypeEditor extends PropertyEditorSupport {
private String format;
public void setFormat(String format) {
this.format = format;
}
public void setAsText(String text) {
if (format != null && format.equals("upperCase")) {
text = text.toUpperCase();
}
ExoticType type = new ExoticType(text);
setValue(type);
}
}
마지막으로, 우리는 ApplicationContext로 새로운 PropertyEditor를 등록하기 위해 CustomEditorConfigurer를 사용하고 필요할때 사용할수 있을것이다.
<bean id="customEditorConfigurer" class="org.springframework.beans.factory.config.CustomEditorConfigurer"> <property name="customEditors"> <map> <entry key="example.ExoticType"> <bean class="example.ExoticTypeEditor"> <property name="format" value="upperCase"/> </bean> </entry> </map> </property> </bean>