Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

[Spring 레퍼런스] 6장 유효성검사(validation), 데이터 바인딩, 타입 변환 #1

이 문서는 개인적인 목적이나 배포하기 위해서 복사할 수 있다. 출력물이든 디지털 문서든 각 복사본에 어떤 비용도 청구할 수 없고 모든 복사본에는 이 카피라이트 문구가 있어야 한다.


6. 유효성검사(validation), 데이터 바인딩, 타입 변환

6.1 소개
비즈니스 로직처럼 유효성검사를 고려할 때 장단점이 있다. 스프링은 유효성검사와 데이터바인딩 중 어느 쪽도 제외하지 않는 유효성 검사(와 데이터바인딩)에 대한 디자인을 제공한다. 구체적으로 말하면 유효성검사는 웹티어에 묶이지 않아냐 하고 쉽게 지역화해야 하고 이용가능한 어떤 밸리데이터(validator)에 연결할 수 있어야 한다. 스프링은 어플리케이션의 모든 레이어에서 기본이 되고 아주 사용하기 편리한 Validator 인터페이스를 제안했다.

데이터바인딩은 어플리케이션의 도메인 모델(또는 사용자 입력을 처리하려고 사용하는 어떤 객체)에 사용자 입력을 동적으로 바인딩하는데 유용하다. 스프링은 이 작업을 하기 위해서 DataBinder를 제공한다. Validator와 DataBinder는 주로 MVC 프레임워크에서 사용되지만 제한이 있는 것은 아닌 validation 패키지를 구성한다.

BeanWrapper는 스프링 프레임워크의 기본 개념이고 많은 곳에서 사용된다. 하지만 BeanWrapper를 직접 사용할 일은 거의 없을 것이다. 하지만 이 문서는 레퍼런스 문서이기 때문에 약간 설명하는 것이 적절하다고 생각한다. 이번 장에서 BeanWrapper를 설명할 것이고 BeanWrapper를 사용할 것이라면 객체에 데이터를 바인딩할 때 사용할 가능성이 높다.

스프링의 DataBinder와 저수준 BeanWrapper는 둘 다 프로퍼티의 값들을 파싱하고 포매팅하는데 PropertyEditor를 사용한다. PropertyEditor 개념은 JavaBeans 명세의 일부이고 역시 이번 장에서 설명한다. 스프링 3는 UI 필드 값을 포매팅하는 고수준 "format" 패키지 뿐만 아니라 일반적인 타입 변환의 기반을 제공하는 "core.convert" 패키지를 도입했다. 이 새로운 패키지들을 PropertyEditor의 더 간단한 대안으로 사용할 것이고 이에 대해서 이번 장에서 얘기할 것이다.


6.2 스프링의 Validator 인터페이스를 사용하는 유효성검사
스프링은 객체의 유효성검사에 사용할 수 있는 Validator 인터페이스를 제공한다. Validator 인터페이스는 유효성검사를 하면서 밸리데이터가 Errors 객체에 유효성검사의 실패내역을 보고할 수 있도록 Errors 객체를 사용해서 동작한다.

작은 데이터를 가진 객체를 생각해 보자.

public class Person {

  private String name;
  private int age;

  // 평범한 getter와 setter...
}

org.springframework.validation.Validator 인터페이스의 다음 두 가지 메서드를 구현해서 Person 클래스에 대한 유효성검사 동작을 제공할 것이다:
  • supports(Class) - 이 Validator가 제공된 Class의 인스턴스를 유효성검사할 수 있는가?
  • validate(Object, org.springframework.validation.Errors) - 주어진 객체에 유효성검사를 하고 유효성검사에 오류가 있는 경우 주어진 객체에 이 오류들을 등록한다.
Validator 구현체는 꽤 직관적이고 특히 스프링 프레임워크가 제공하는 ValidationUtils 헬퍼 클래스를 알고 있다면 더욱 그렇다.

public class PersonValidator implements Validator {

  /**
  * 이 Validator는 단순히 Person 인스턴스를 유효성검사한다
  */
  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(..) 메서드는 'name' 프로퍼티가 null이거나 빈(empty) 문자열일 때 'name' 프로퍼티를 거절하는 데 사용한다. 앞에서 보여준 예제와 함께 무슨 기능을 제공하는지 보려면 ValidationUtils 클래스의 Javadoc을 봐라.

풍부한(rich) 객체에서 내장된 각 객체들의 유효성을 검사하려고 하나의 Validator 클래스를 구현하는 것이 확실히 가능하지만 객체 자신만의 Validator 구현체에서 객체에 내장된 각 클래스에 대한 유효성검사 로직을 은닉화하는 것이 더 나을 것이다. '풍부한' 객체의 간단한 예는 두 개의 String 프로퍼티(이름과 성)와 하나의 복잡한 Address 객체로 구성된 Customer이다. Address 객체들은 Customer 객체들과 관계없이 사용될 것이므로 별도의 AddressValidator를 구현했다. 복사-붙혀넣기를 사용하지 않고 AddressValidator 클래스내에 포함된 로직을 재사용하려고 CustomerValidator를 원한다면 다음과 같이 CustomerValidator내에서 AddressValidator를 의존성 주입하거나 인스턴스화 해서 사용할 수 있다.

public class CustomerValidator implements Validator {

  private final Validator addressValidator;

  public CustomerValidator(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;
  }

  /**
  * 이 Validator는 Customer 인스턴스의 유효성을 검사하고 Customer의 모든 하위 클래스도 유효성 검사한다
  */
  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();
    }
  }
}


유효성 검사 오류는 밸리데이터에 전달한 Errors 객체에 보고된다. 스프링 웹 MVC에서는 오류 메세지를 검사하려고 <spring:bind/> 태그를 사용할 수 있지만 당연히 직접 오류 객체를 검사할 수도 있다. 이 메서드가 제공하는 더 자세한 내용은 Javadoc에 나와 있다.


6.3 오류 메시지에 대한 코드 처리
데이터바인딩과 유효성검사에 대해서 이야기했다. 마지막으로 유효성 오류에 대응되는 출력 메세지에 대해서 얘기해야 한다. 위에서 본 예제에서 name와 age 필드를 거절했다. MessageSource를 사용해서 오류 메세지를 출력하려면 필드(이 경우에는 'name'와 'age')를 거절했을 때 전달받은 오류 코드를 사용해야 할 것이다. rejectValue를 호출하거나(직접적이든 간접적이든 ValidationUtils같은 클래스를 사용해서) Errors 인터페이스의 다른 reject 메서드 중의 하나를 호출했을 때 기반이 되는 구현체는 전달한 코드뿐만 아니라 추가적인 다수의 오류 코드를 등록한다. 어떤 오류 코드를 등록하는 지는 사용하는 MessageCodesResolver가 결정한다. 기본적으로 DefaultMessageCodesResolver를 사용한다. 예를 들어 DefaultMessageCodesResolver는 전달한 코드와 메세지를 등록하고 reject 메서드에 전달한 필드명을 포함한 메세지를 등록한다. 그래서 rejectValue("age", "too.darn.old")를 사용해서 필드를 거절하는 경우 스프링은 too.darn.old 코드 이외에 too.darn.old.age 와 too.darn.old.age.int도 등록할 것이다.(그래서 첫번째 것은 필드 명을 담고 있고 두번째 것은 필드의 타입을 담고 있다.) 이는 대상 오유 메세지같은 부분에서 개발자를 도와주는 편리한 작업이다.

MessageCodesResolver와 기본 전략에 대한 더 자세한 내용은 각각 MessageCodesResolverDefaultMessageCodesResolver 온라인 Javadoc에서 찾을 수 있다.


6.4 빈 조작과 BeanWrapper
org.springframework.beans 패키지는 Sun의 자바 빈 표준을 충실히 따른다. JavaBean은 아규먼트가 없는 기본 생성자를 가진 클래스이고 (한 예로써) bingoMadness라는 프로퍼티는 setBingoMadness(..) setter 메서드와 getBingoMadness() getter 메서드를 가지는 네이밍 관례를 따른다. JavaBens과 명세에 대한 더 자세한 정보를 알고 싶으면 Sun의 웹사이트 ( java.sun.com/products/javabeans)를 참조해라.

beans 패키지에서 아주 중요한 클래스는 BeanWrapper 인터페이스와 그에 대한 구현체(BeanWrapperImpl)이다. Javadoc에 나와있듯이 BeanWrapper는 프로퍼티 값(개별적으로나 한꺼번에)을 설정하고 가져오는 기능과 프로퍼티 드스크립터를 가져오는 기능이나 프로퍼티가 읽을 수 있는지 쓸 수 있는지 결정하기 위해 쿼리할 수 있는 기능을 제공한다. BeanWrapper는 무한 계층까지 하위 프로퍼티에서 프로퍼티를 설정하는 것이 가능하도록 중첩된 프로퍼티도 지원한다. 그리고 BeanWrapper는 대상 클래스에 지원 코드를 두지 않고도 표준 자바빈 PropertyChangeListeners와 VetoableChangeListeners를 추가하는 기능도 지원한다. 마지막으로 가장 중요한 것은 BeanWrapper가 색인된 프로퍼티를 설정하는 지원을 제공한다는 것이다. BeanWrapper는 보통 어플리케이션 코드에서 직접 사용하지 않고 DataBinder와 BeanFactory에서 사용한다.

BeanWrapper의 동작방식은 그 이름이 어느 정도 알려주고 있다. 프로퍼티를 설정하고 획득하는 것처런 해당 빈에 액셩을 수행하기 위해서 빈을 감싼다.


6.4.1 기본적인 프로퍼티와 중첩된 프로퍼티를 설정하고 가져오기
프로퍼티를 설정하고 가져오는 것은 setPropertyValue(s)와 getPropertyValue(s) 메서드를 사용해서 이뤄진다. 둘 다 다수의 오버로드된 메서드들이 있다. 자세한 내용은 스프링 자바독에 모두 설명되어 있다. 객체의 프로퍼티를 나타내는 여러 가지 관례가 있다는 사실은 중요하다. 다음은 몇가지 예제이다.

Table 6.1. 프로퍼티 예제
표현식설명
namegetName()나 isName()나 setName(..) 메서드와 대응되는 name 프로퍼티를 나타낸다.
account.nameaccount 프로퍼티의 중첩된 name 프로퍼티를 나타낸다. 예를 들면 getAccount().setName()나 getAccount().getName()에 대응된다.
account[2]색인된 account 프로퍼티의 세번째 요소를 나타낸다. 색인된 프로퍼티는 array, list나 자연스럽게 정렬된 컬렉션이 될 수 있다.
account[COMPANYNAME]Map 프로퍼티 account의 COMPANYNAME 키로 찾은 값을 나타낸다.












아래에서 프로퍼티를 얻거나 설정하는 BeanWrapper 동작 예제를 보여줄 것이다.

(BeanWrapper를 직접 사용해서 작업하지 않는다면 다음 부분은 아주 중요한 것은 아니다. DataBinder와 BeanFactory나 이들의 어떤 구현체를 그냥 사용할 것이라면 PropertyEditors에 대한 섹션으로 건너뛰어도 좋다.)

다음 두 클래스를 보자.

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 String name;
  private float salary;

  public String getName() {
    return this.name;
  }
  public void setName(String name) {
    this.name = name;
  }
  public float getSalary() {
    return salary;
  }
  public void setSalary(float salary) {
    this.salary = salary;
  }
}


다음 코드는 인스턴스화된 Companies와 Employees의 프로퍼티를 어떻게 획득하고 조작하는 가를 보여주는 예제이다.

BeanWrapper company = BeanWrapperImpl(new Company());
// 회사이름을 설정한다..
company.setPropertyValue("name", "Some Company Inc.");
// ... 또는 다음과 같이 할 수 있다:
PropertyValue value = new PropertyValue("name", "Some Company Inc.");
company.setPropertyValue(value);

// 감독관을 생성하고 회사에 연결한다:
BeanWrapper jim = BeanWrapperImpl(new Employee());
jim.setPropertyValue("name", "Jim Stravinsky");
company.setPropertyValue("managingDirector", jim.getWrappedInstance());

// company를 통해 managingDirector의 봉급을 획득한다
Float salary = (Float) company.getPropertyValue("managingDirector.salary");



6.4.2 내장 PropertyEditor 구현체
스프링은 Object와 String간의 변환에 PropertyEditors의 개념을 사용한다. 생각해보면 때때로 객체 자체와는 다른 방법으로 프로퍼티를 표현할 수 있는 손쉬운 방법이다. 예를 들어 Date는 사람이 읽을 수 있게 표현할 수 있고 (String '2007-14-09'처럼) 동시에 여전히 사람이 읽을 수 있는 형식을 다시 원래의 날짜로 변환할 수 있다.(또는 더 낫다: 사람이 읽을 수 있는 형식의 어떤 날짜도 다시 Date 객체로 변환할 수 있다.) 이 동작은 java.beans.PropertyEditor 타입의 커스텀 에디터를 등록함으로써 이뤄질 수 있다. BeanWrapper나 이전 챕터에서 얘기했던 대안적인 특정 IoC 컨테이너에 커스텀 에디터를 등록하면 어떻게 프로퍼티를 원하는 타입으로 변환하는 지 알려준다. 더 자세한 내용은 Sun이 제공하는 java.beans 패키지의 Javadoc에서 PropertyEditors 부분을 읽어봐라.

스프링에서 프로퍼티 수정을 사용하는 몇가지 예제:
  • PropertyEditors를 사용해서 빈에 프로퍼티를 설정한다. XML 파일에 선언한 어떤 빈의 프로퍼티 값으로 java.lang.String을 사용했을 때 스프링은 (해당 프로퍼티의 setter가 Class-parameter를 가지고 있다면)파라미터를 Class 객체로 처리하려고 ClassEditor를 사용할 것이다.
  • 스프링 MVC 프레임워크에서 HTTP 요청 파라미터의 파싱은 CommandController의 모든 하위클래스에 수동으로 연결할 수 있는 모든 종류의 PropertyEditors를 사용해서 이뤄진다.
스프링은 쉽게 사용할 수 있도록 다수의 내장 PropertyEditors를 가진다. 이 내장 PropertyEditors는 아래 목록에 나와있고 이 모두는 org.springframework.beans.propertyeditors 패키지에 존재한다. (아래에 표시했듯이)모두는 아니지만 대부분은 BeanWrapperImpl의 기본으로 등록된다. 프로퍼티 데이터가 몇가지 방법으로 설정할 수 있는 곳에서도 당연히 기본값은 자신만의 번형으로 오버라이드해서 등록할 수 있다.

Table 6.2. 내장 PropertyEditors
Class설명
ByteArrayPropertyEditor바이트 배열에 대한 에디터. 문자열은 간단하게 대응되는 바이트 표현으로 변환될 것이다. BeanWrapperImpl의 기본값으로 등록된다.
ClassEditor클래스를 나타내는 문자열을 실제 클래스로 파싱하거나 그 반대로 파싱한다. 클래스를 찾지 못하면 IllegalArgumentException를 던진다. BeanWrapperImpl의 기본값으로 등록된다.
CustomBooleanEditorBoolean 프로퍼티에 대해 커스터마이징할 수 있는 프로퍼티 데이터다. BeanWrapperImpl의 기본값으로 등록되지만 커스텀 에디터처럼 커스텀 인스턴스를 등록함으로써 오버라이드할 수 있다.
CustomCollectionEditor컬렉션에 대한 프로퍼티 에디터로 모든 소스 Collection를 전달한 타겟 Collection 타입으로 변환한다.
CustomDateEditorjava.util.Date에 대한 커스터마이징 할 수 있는 프로퍼티 에디터로 커스텀 DateFormat을 지원한다. 기본값으로 등록되지 않는다. 적절한 형식으로 필요한 만큼 사용자가 등록해야 한다.
CustomNumberEditorInteger, Long, Float, Double같은 숫자타입의 하위클래스에 대한 커스마타이징할 수 있는 프로퍼티 에디터이다. BeanWrapperImpl의 기본값으로 등록되지만 커스텀 에디터처럼 커스텀 인스턴스를 등록해서 오바라이드할 수 있다.
FileEditor문자열을 java.io.File 객체로 처리할 수 있다. BeanWrapperImpl의 기본값으로 등록된다.
InputStreamEditorInputStream 프로퍼티를 문자열로 직접 설정할 수 있도록 텍스트 문자열을 받아서 InputStream을 생성하는(중간에 ResourceEditor와 Resource를 통해서) 단방향 프로퍼티 에디터이다. 기본 사용법은 InputStream를 닫지 않을 것이다. BeanWrapperImpl의 기본값으로 등록된다.
LocaleEditor문자열을 Locale 객체로 처리하거나 그 반대로 할 수 있다.(문자열 형식은 Locale의 toString() 메서드가 제공하는 형식과 같은 [language]_[country]_[variant]이다.) BeanWrapperImpl의 기본값으로 등록된다.
PatternEditor문자열을 JDK 1.5 Pattern 객체로 처리하거나 그 반대로 처리할 수 있다.
PropertiesEditor문자열(java.lang.Properties 클래스의 Javadoc에서 정의된 것과 같은 형식으로 포매팅된)을 Properties 객체로 변환할 수 있다.BeanWrapperImpl의 기본값으로 등록된다.
StringTrimmerEditor스트림을 trim하는 프로퍼티 에디터이다. 선택적으로 비어있는 문자열을 null 값으로 변형할 수도 있다. 기본적으로는 등록되지 않는다. 필요에 따라 사용자가 등록해야 한다.
URLEditorURL의 문자열 표현을 실제 URL 객체로 처리할 수 있다. BeanWrapperImpl의 기본값으로 등록된다.











































스프링은 필요한 프로퍼티 에디터에 검색경로를 설정하는데 java.beans.PropertyEditorManager를 사용한다. 검색 경로는 Font와 Color나 대부분의 프리미티브 타입같은 타입에 대한 PropertyEditor 구현체를 포함하는 sun.bean.editors를 포함할 수도 있다. 다루는 클래스와 같은 패키지에 있고 해당 클래스와 같은 이름이면서 'Editor'가 붙어있으면 표준 자바빈 기반은 자동적으로 PropertyEditor 클래스(명시적으로 등록하지 않아도)를 검색할 것이다. 예를 들어 FooEditor 클래스를 인식하기에 충분하고 Foo 타입에 대한 프로퍼티를 위한 PropertyEditor로 사용되려면 다음 클래스와 패키지 구조를 갖고 있어야 한다.

com
  chank
    pop
      Foo
      FooEditor   // Foo 클래스에 대한 PropertyEditor


여기서도 표준 BeanInfo 자바빈 메카니즘을 사용할 수 있다.(아주 자세하지는 않지만 여기에 설명되어 있다.) 다음은 연관된 클래스의 프로퍼티와 하나 이상의 PropertyEditor 인스턴스를 명시적으로 등록하기 위해 BeanInfo 메카니즘을 사용하는 예제다.

com
  chank
    pop
      Foo
      FooBeanInfo   // Foo 클래스에 대한 BeanInfo


다음은 참조한 FooBeanInfo 클래스의 자바 소스코드다. 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());
    }
  }
}



6.4.2.1 추가적인 커스텀 PropertyEditors 등록
빈 프로퍼티를 문자열로 설정하는 경우 스프링 IoC 컨테이너는 이 문자열을 복잡한 프로퍼티 타입으로 변환하는데(예를 들어 문자열로 된 클래스명을 실제 Class 객체로 변환한다.) 표준 자바빈 PropertyEditors를 사용한다. 게다가 자바 표준 자바빈 PropertyEditor의 검색 메카니즘은 클래스의 PropertyEditor에 적절한 이름을 붙히고 자동으로 찾아지도록 클래스와 같은 패키지 안에 둔다.

다른 커스텀 PropertyEditors를 등록해야 하는 경우 여러 가지 메카니즘을 사용할 수 있다. 보통 편리하지도 않고 추천하지도 않지만 가장 수동적인 접근은 BeanFactory 참조를 가지고 있다고 가정했을 때 ConfigurableBeanFactory 인터페이스의 registerCustomEditor() 메서드를 사용하는 것이다. 약간 더 편리한 메카니즘은 CustomEditorConfigurer라는 전용 빈 팩토리 후처리자를 사용하는 것이다. 빈 팩토리 후처리자를 BeanFactory 구현체와 함께 사용할 수 있기는 하지만 CustomEditorConfigurer는 중첩 프로퍼티 설정을 가지고 있으므로 비슷한 방법으로 다른 빈에 배포되고 자동으로 찾아서 적용되는 ApplicationContext와 함께 사용하기를 간단히 추천한다.

모든 빈 팩토리와 어플리케이션 컨텍스트는 프로퍼티 변환을 위해 BeanWrapper를 사용하기 위해 자동으로 다수의 내장된 프로퍼티 에디터를 사용한다. BeanWrapper가 등록하는 표준 프로퍼티 에디터는 이전 섹션에 나와 있다. 게다가 ApplicationContexts는 해당 어플리케이션 컨텍스트 타입에 적절한 방법으로 리소스 검색을 위해 에디터를 덮어쓰거나 추가적으로 다수의 에디터를 추가하기도 한다.

문자열의 프로퍼티 값을 프로퍼티의 실제 복잡한 타입으로 변환하는데 표준 자바빈 PropertyEditor 인스턴스를 사용한다. ApplicationContext에 추가적인 PropertyEditor 인스턴스를 편리하게 추가하기 위해 빈 팩토리 후처리자인 CustomEditorConfigurer를 사용한다.

ExoticType를 프로퍼티로 설정할 필요가 있는 사용자 클래스 ExoticType와 DependsOnExoticType 클래스를 고려해 봐라.

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;
  }
}


PropertyEditor가 뒤에서 실제 ExoticType 인스턴스로 변환할 type 프로퍼티를 설정시에 문자열로 할당할 수 있기를 원한다.

<bean id="sample" class="example.DependsOnExoticType">
  <property name="type" value="aNameForExoticType"/>
</bean>


PropertyEditor 구현체는 다음과 같을 것이다.

// 문자열 표현을 ExoticType 객체로 변환한다
package example;

public class ExoticTypeEditor extends PropertyEditorSupport {

  public void setAsText(String text) {
    setValue(new ExoticType(text.toUpperCase()));
  }
}



마지막으로 ApplicationContext에 새로운 PropertyEditor를 등록하려고 CustomEditorConfigurer를 사용하고 필요에 따라 사용할 수 있다.

<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
  <property name="customEditors">
    <map>
      <entry key="example.ExoticType" value="example.ExoticTypeEditor"/>
    </map>
  </property>
</bean>



PropertyEditorRegistrars의 사용
스프링 컨테이너에 프로퍼티 에디터를 등록하는 또다른 메카니즘은 PropertyEditorRegistrar를 생성하고 사용하는 것이다. 이 인터페이스는 여러 가지 다른 상황에서 같은 프로퍼티 에디터의 세트를 사용해야할 때 특히 유용하다. 즉 대응되는 담당자(registrar)를 작성하고 각 상황에서 재사용한다. PropertyEditorRegistrars는 스프링의 BeanWrapper(와 DataBinder)가 구현한 PropertyEditorRegistry 인터페이스와 결합해서 동작한다. PropertyEditorRegistrars는 setPropertyEditorRegistrars(..)라는 프로퍼티를 노출하는 CustomEditorConfigurer(여기에 설명되어 있다)와 결합해서 사용할 때 특히 편리하다. 이 방법에서 CustomEditorConfigurer에 추가된 PropertyEditorRegistrars는 쉽게 DataBinder와 Spring MVC Controllers와 공유될 수 있다. 게다가 이는 커스텀 에디터에서 동기화를 피한다. PropertyEditorRegistrar는 각각의 빈을 생성하는 시도에서 새로운 PropertyEditor 인스턴스를 생성할 것이다.

Using a PropertyEditorRegistrar is perhaps best illustrated with an example. First off, you need to create your own PropertyEditorRegistrar implementation:

PropertyEditorRegistrar를 사용하는 방법은 예제로 설명하는 것이 가장 좋을 것이다. 먼저 자신만의 PropertyEditorRegistrar 구현체를 생성해야 한다.

package com.foo.editors.spring;

public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar {

  public void registerCustomEditors(PropertyEditorRegistry registry) {

    // PropertyEditor 인스턴스가 생성되기를 기대한다
    registry.registerCustomEditor(ExoticType.class, new ExoticTypeEditor());

    // 여기서 커스텀 프로퍼티 에디터를 필요한만큼 등록할 수 있다
  }
}



PropertyEditorRegistrar 구현체에 대한 예제는 org.springframework.beans.support.ResourceEditorRegistrar를 봐라. registerCustomEditors(..) 메서드의 PropertyEditorRegistrar 구현체가 각 프로퍼티 에디터의 새로운 인스턴스를 어떻게 생성하는 지 봐라.

그 다음 CustomEditorConfigurer를 설정하고 CustomPropertyEditorRegistrar 인스턴스를 PropertyEditorRegistrar에 주입한다.

<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
  <property name="propertyEditorRegistrars">
    <list>
      <ref bean="customPropertyEditorRegistrar"/>
    </list>
  </property>
</bean>

<bean id="customPropertyEditorRegistrar"
  class="com.foo.editors.spring.CustomPropertyEditorRegistrar"/>



마지막으로 이번 챕터의 주제에서 약간 벗어나서 Spring의 MVC 웹 프레임워크를 사용하는 경우 데이터 바인딩 Controllers (SimpleFormController 같은)와 함께 PropertyEditorRegistrars를 사용한다면 아주 편리하다. 다음은 initBinder(..) 메서드의 구현체에서 PropertyEditorRegistrar를 사용하는 예제다.

public final class RegisterUserController extends SimpleFormController {

  private final PropertyEditorRegistrar customPropertyEditorRegistrar;

  public RegisterUserController(PropertyEditorRegistrar propertyEditorRegistrar) {
    this.customPropertyEditorRegistrar = propertyEditorRegistrar;
  }

  protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder)
    throws Exception {
      this.customPropertyEditorRegistrar.registerCustomEditors(binder);
  }

  // User를 등록하는 등의 다른 메서드
}



이러한 방식의 PropertyEditor 등록으로 코드는 간결해 지고(initBinder(..)의 구현체는 딱 한줄 뿐이다!) 공통 PropertyEditor 등록코드를 클래스에 은닉화해서 필요한만큼의 많은 Controllers에서 공유할 수 있다.
2012/08/20 00:39 2012/08/20 00:39