[Info]Tags categorized posts and contents patterns..

[AJAX] Ajax Code E xamples.. [Book] About the book.. [CSS] CSS Code E xamples.. [DB] Sql Code E xamples.. [DEV] All development stor...

2016년 4월 25일 월요일

[JAVA] 6장 유효성검사(validation), 데이터 바인딩, 타입 변환 #2..

출처 : Outsider's Dev Story https://blog.outsider.ne.kr/

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

6.5 Spring 3 타입 변환
스프링 3는 일반적인 타입변환 시스템을 위해 core.convert 패키지를 도입했다. 타입변환 시스템은 타입변환 로직을 구현하는 SPI와 런타임시에 타입변환을 실행하는 API를 정의한다. 스프링 컨테이너에서 구체적인 빈 프로퍼티값을 필요한 프로퍼티 타입으로 변환하는 프로퍼티 에디터 대신에 타입변환 시스템을 사용할 수 있다. 어플리케이션내에서 타입변환이 필요한 어디서든 퍼블릭 API를 사용할 수 있다.

6.5.1 Converter SPI
타입변환 로직을 구현하는 SPI는 간단하고 강타입(strongly typed)이다.


1
2
3
4
5
6
7
8
9
Java

package org.springframework.core.convert.converter;

public interface Converter<S, T> {

  T convert(S source);

}

자신만의 Converter를 만들려면 위의 인터페이스를 구현하면 된다. 파라미터 S는 변환되기 전의 타입이고 파라미터 T는 변환할 타입이다. convert(S)를 호출할 때 source 아규먼트는 null이 아니라는 것을 보장해야 한다. 작성한 Converter는 변환에 실패했을 때 Exception을 던질 것이다. source의 값이 유효하지 않은 경우 IllegalArgumentException을 던져야 한다. 작성한 Converter 구현체가 쓰레드 세이프하도록 해야 한다.

core.convert.support 패키지에 핀리한 여러 가지 컨버터 구현체가 있다. 이 구현체들은 문자열을 숫자나 다른 일반적인 타입으로 변환하는 컨버터가 포한되어 있다. Converter 구현의 예제로 StringToInteger를 보자.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Java

package org.springframework.core.convert.support;

final class StringToInteger implements Converter<String, Integer> {

  public Integer convert(String source) {
    return Integer.valueOf(source);
  }

}

6.5.2 ConverterFactory
String을 java.lang.Enum 객체로 변환하는 등 전체 클래스 계층에서 변환로직을 한 곳에 모으려고 한다면 ConverterFactory를 구현해라.

1
2
3
4
5
6
7
Java

package org.springframework.core.convert.converter;

public interface ConverterFactory<S, R> {
  <T extends R> Converter<S, T> getConverter(Class<T> targetType);
}

파라미터 S는 변환하기 전의 타입이고 파라미터 R은 변환할 클래스의 범위를 정의하는 기본타입이다. 이제 getConverter(Class<T>)를 구현해라. T는 R의 슈퍼클래스다.
StringToEnum ConverterFactory 예제를 보자.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Java

package org.springframework.core.convert.support;

final class StringToEnumConverterFactory implements ConverterFactory<String, Enum> {

  public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {
    return new StringToEnumConverter(targetType);
  }

  private final class StringToEnumConverter<T extends Enum> implements Converter<String, T> {

    private Class<T> enumType;

    public StringToEnumConverter(Class<T> enumType) {
      this.enumType = enumType;
    }

    public T convert(String source) {
      return (T) Enum.valueOf(this.enumType, source.trim());
    }
  }
}

6.5.3 GenericConverter
세련 된 Converter 구현체가 필요하다면 GenericConverter 인터페이스를 고려해봐라. 훨씬 유연하면서도 타입 제약이 적은 GenericConverter는 여러 가지 타입의 소스와 타겟간의 변환을 지원한다. 게다가 GenericConverter는 자신만의 변환 로직을 구현할 때 소스와 타겟 필드 컨텍스트를 사용할 수 있게 해준다. 이러한 컨텍스트로 필드 어노테이션이나 필드정의에 선언된 제너릭 정보로 타입변환을 할 수 있다.

1
2
3
4
5
6
7
8
Java

package org.springframework.core.convert.converter;

public interface GenericConverter {
  public Set<ConvertiblePair> getConvertibleTypes();
  Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}

GenericConverter를 구현하려면 지원하는 source->target 타입의 쌍을 반환하는 getConvertibleTypes()를 정의하고 변환 로직을 위해 convert(Object, TypeDescriptor, TypeDescriptor)를 구현한다. 소스 TypeDescriptor는 변환될 값이 있는 소스 필드에 접근하게 해주고 타겟 TypeDescriptor는 변환된 값이 할당될 타겟 필드에 접근하게 해준다.

자바 배열과 컬렉션을 변환해주는 컨버터는 GenericConverter의 좋은 예제다. 이 ArrayToCollectionConverter는 컬렉션의 엘리먼트 타입을 위한 타겟 컬렉션 타입을 선언한 필드를 가진다. 이는 타겟 필드에 컬렉션을 할당하기 전에 소스 배열의 각 엘리먼트를 컬렉션 엘리먼트 타입으로 변환한다.



Note
GenericConverter은 훨씬 복잡한 SPI 인터페이스이므로 필요할때만 사용해야 한다. 기본적인 타입 변환이 필요하다면 Converter나 ConverterFactory를 사용해라.

6.5.3.1 ConditionalGenericConverter
때 로는 지정한 상태가 참일 경우에만 Converter를 실행하고 싶을 것이다. 예를 들어 타겟 필드에 어노테이션을 지정했을 때만 Converter를 실행하고 싶을 것이다. 또는 정적 valueOf 메서드처럼 지정한 메서드가 타켓클래스에 정의되었을 때만 Converter를 실행하기를 원할 것이다. GenericConverter의 하위인터페이스인 ConditionalGenericConverter는 이러한 커스텀 크리테리아 검사를 정의할 수 있다.


1
2
3
4
5
Java

public interface ConditionalGenericConverter extends GenericConverter {
  boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}

퍼시스턴트 엔티티 식별자와 엔티티 참조간의 변환을 하는 EntityConverter가 ConditionalGenericConverter의 좋은 예이다. 이러한 EntityConverter는 타겟 엔티티 타입이 정적 finder 메서드(예: findAccount(Long))를 정의했을 때만 수행된다. matches(TypeDescriptor, TypeDescriptor)의 구현체에서 이러한 finder 메서드 검사를 수행한다.

6.5.4 ConversionService API
ConversionService는 런타임시에 타입 변환 로직을 실행하는 통일된 API를 정의한다. 때로는 이러한 퍼사드 인터페이스뒤에서 컨버터가 실행된다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Java

package org.springframework.core.convert;

public interface ConversionService {
  boolean canConvert(Class<?> sourceType, Class<?> targetType);

  <T> T convert(Object source, Class<T> targetType);

  boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);

  Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}

대부분의 ConversionService 구현체는 컨버터를 등록하는 SPI를 제공하는 ConverterRegistry도 구현하고 있다. 내부적으로 ConversionService 구현체는 타입변환 로직 수행을 등록된 컨버터에 위임한다.

신뢰할 수 있는 ConversionService 구현체는 core.convert.support 패키지에 있다. GenericConversionService는 대부분에 환경에서 사용할 수 있는 범용적인 구현체이다. ConversionServiceFactory는 공통적인 ConversionService 설정을 생성하는 편리한 팩토리를 제공한다.

6.5.5 ConversionService 설정
ConversionService 는 어플리케이션 구동시에 인스턴스화되고 여러 쓰레드 사이에서 공유되도록 설계된 무상태의 객체이다. 스프링 어플리케이션에서는 보통 스프링 컨테이너(또는 ApplicationContext)마다 ConversionService 인스턴스를 설정한다. 설정한 ConversionService를 스프링이 선택해서 프레임워크가 타입변환을 수행해야 할 때마다 사용할 것이다. 이 ConversionService를 어떤 빈에라도 주입해서 직접 호출할 수도 있다.



Note
스프링에 등록된 ConversionService가 없으면 원래의 PropertyEditor기반 시스템을 사용한다.

conversionService id의 다음 빈 정의를 추가해서 스프링에 기본 ConversionService를 등록한다.


1
2
3
Xml

<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean"/>

기본 ConversionService는 문자열, 숫자, 이넘(enums), 컬렉션, 맵 등의 타입을 변환한다. converters 프로퍼티를 설정해서 자신의 커스텀 컴버터로 기본 컨버터를 보완하거나 오버라이드할 수 있다. 프로퍼티 값은 Converter, ConverterFactory, GenericConverter 인터페이스를 구현할 것이다.

1
2
3
4
5
6
7
8
9
Xml

<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
  <property name="converters">
    <list>
      <bean class="example.MyCustomConverter"/>
    </list>
  </property>
</bean>

스프링 MVC 어플리케이션에서 ConversionService를 사용하는 것도 일반적이다. < mvc:annotation-driven/>로 사용하는 방법은 Section 6.6.5, “Spring MVC에서의 포매팅 설정”를 봐라.

변환하는 과정에서 포매팅을 적용해야 하는 경우도 있다. FormattingConversionServiceFactoryBean를 사용하는 방법은 Section 6.6.3, “FormatterRegistry SPI”를 봐라.

6.5.6 프로그래밍적인 ConversionService의 사용
다른 빈에 ConversionService 인스턴스의 참조를 주입해서 프로그래밍적으로 ConversionService 인스턴스를 사용할 수 있다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Java

@Service
public class MyService {

  @Autowired
  public MyService(ConversionService conversionService) {
    this.conversionService = conversionService;
  }

  public void doIt() {
    this.conversionService.convert(...)
  }
}

6.6 Spring 3 필드 포매팅
이전 섹션에서 얘기했듯이 core.convert는 범용적인 타입변환 시스템이다. 이는 한 타입에서 다른 타입으로 변환하는 로직을 구현하는 강타입의 Converter SPI처럼 통일된 ConversionService API를 제공한다. 스프링 컨테이너는 빈 프로퍼티의 값을 바인딩하는데 이 시스템을 사용한다. 게다가 스프링 표현언어 (SpEL)와 DataBinder는 둘 다 필드 값을 바인딩 하는데 이 시스템을 사용한다. 예를 들어 expression.setValue(Object bean, Object value)를 실행하기 위해 SpEL이 Short를 Long으로 강제해야 할 때 core.convert 시스템이 강제한다.

웹 어플리케이션이나 데스크톱 어플리케이션같은 전형적인 클라이언트 환경에서의 타입변환 요구사항을 생각해 보자. 이러한 환경에서는 보통 String을 클라이언트의 포스트백(postback) 과정을 지원하도록 변환하고 뷰 렌더링 과정을 위해 다시 String로 변환한다. 또한 때로는 문자열 값을 로컬라이징할 필요가 있다. 더 일반적인 core.convert Converter SPI는 포매팅같은 요구사항을 직접 다루지 않는다. 이러한 것들을 직접 다루기 위해 스프링 3는 PropertyEditor 대신 클라이언트 환경에서 사용할 수 있는 간단하고 신뢰할 수 있으며 편리한 Formatter SPI를 도입했다.

보통 범용적인 타입 변환 로직을 구현할 때 Converter SPI를 사용하는데 java.util.Date와 java.lang.Long 간에 변환을 하는 경우이다. 웹 어플리케이션같은 클라이언트 환경에서 작업하고 로컬라이징된 필드값을 파싱해서 출력해야 할 때 Formatter SPI를 사용해라. ConversionService는 두 SPI에 대한 일관된 타입변환 API를 제공한다.

6.6.1 Formatter SPI
필드 포매팅 로직을 구현하는 Formatter SPI는 간단하면서도 강타입이다.


1
2
3
4
5
6
Java

package org.springframework.format;

public interface Formatter<T> extends Printer<T>, Parser<T> {
}

Formatter는 Printer와 Parser 인터페이스를 상속받는다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Java

public interface Printer<T> {
  String print(T fieldValue, Locale locale);
}


Java

import java.text.ParseException;

public interface Parser<T> {
  T parse(String clientValue, Locale locale) throws ParseException;
}

위의 Formatter 인터페이스를 구현해서 자신만의 Formatter를 만들 수 있다. 파라미터 T는 포매팅할 객체의 타입으로 java.util.Date등이 될 수 있다. T의 인스턴스를 클라이언트 로케일(locale)로 출력하는 print()를 구현한다. 클라이언트 로케일이 반환한 포매팅된 표현에서 T의 인스턴스를 파싱하는 parse()를 구현해라. 작성한 Formatter가 파싱에 실패하면 ParseException나 IllegalArgumentException를 던져야 한다. Formatter가 쓰레드세이프하게 구현되도록 신경써라.

여러가지 포매터 구현체는 편리하게 format 하위패키지 아래 있다. number는 java.text.NumberFormat를 사용해서 java.lang.Number 객체를 포매팅하는 NumberFormatter, CurrencyFormatter, PercentFormatter를 제공한다. datetime 패키지는 java.util.Date 객체를 java.text.DateFormat로 포매팅하는 DateFormatter를 제공한다. datetime.joda 패키지는 Joda 시간 라이브러리에 기반해서 포괄적인 datetime 포매팅을 지원한다. Formatter 구현체의 예제로 DateFormatter를 보자.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Java

package org.springframework.format.datetime;

public final class DateFormatter implements Formatter<Date> {

  private String pattern;

  public DateFormatter(String pattern) {
    this.pattern = pattern;
  }

  public String print(Date date, Locale locale) {
    if (date == null) {
      return "";
    }
    return getDateFormat(locale).format(date);
  }

  public Date parse(String formatted, Locale locale) throws ParseException {
    if (formatted.length() == 0) {
      return null;
    }
    return getDateFormat(locale).parse(formatted);
  }

  protected DateFormat getDateFormat(Locale locale) {
    DateFormat dateFormat = new SimpleDateFormat(this.pattern, locale);
    dateFormat.setLenient(false);
    return dateFormat;
  }

}

스프링 개발팀은 커뮤니티가 포매터를 추가하는 것을 환영하고 있다. 포매터에 공헌하려면 http://jira.springframework.org를 봐라.

6.6.2 어노테이션 기반의 포매팅
다음과 같이 필드 타입이나 어노테이션으로 필드 포매팅을 설정할 수 있다. 포매터에 어노테이션을 바인딩하려면 AnnotationFormatterFactory를 구현해라.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Java

package org.springframework.format;

public interface AnnotationFormatterFactory<A extends Annotation> {

  Set<Class<?>> getFieldTypes();

  Printer<?> getPrinter(A annotation, Class<?> fieldType);

  Parser<?> getParser(A annotation, Class<?> fieldType);

}

파라미터 A는 org.springframework.format.annotation.DateTimeFormat같은 포매팅 로직과 연결될 annotationType 필드이다. getFieldTypes()는 어노테이션이 사용된 필드의 타입을 반환한다. getPrinter()는 어노테이션이 붙은 필드의 값을 출력하는 Printer를 반환한다. getParser()는 어노테이션이 붙은 필트의 clientValue를 파싱하는 Parser를 반환한다.

아래의 AnnotationFormatterFactory 구현예제는 포매터에 NumberFormat 어노테이션을 붙혔다. 이 어노테이션은 숫자형식과 패틴을 지정하게 한다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Java

public final class NumberFormatAnnotationFormatterFactory
      implements AnnotationFormatterFactory<NumberFormat> {

  public Set<Class<?>> getFieldTypes() {
    return new HashSet<Class<?>>(asList(new Class<?>[] {
      Short.class, Integer.class, Long.class, Float.class,
      Double.class, BigDecimal.class, BigInteger.class }));
  }

  public Printer<Number> getPrinter(NumberFormat annotation, Class<?> fieldType) {
    return configureFormatterFrom(annotation, fieldType);
  }

  public Parser<Number> getParser(NumberFormat annotation, Class<?> fieldType) {
    return configureFormatterFrom(annotation, fieldType);
  }

  private Formatter<Number> configureFormatterFrom(NumberFormat annotation,
                                                     Class<?> fieldType) {
    if (!annotation.pattern().isEmpty()) {
      return new NumberFormatter(annotation.pattern());
    } else {
      Style style = annotation.style();
      if (style == Style.PERCENT) {
        return new PercentFormatter();
      } else if (style == Style.CURRENCY) {
        return new CurrencyFormatter();
      } else {
        return new NumberFormatter();
      }
    }
  }
}

필드에 @NumberFormat 어노테이션을 붙혀서 포매팅을 실행한다.

1
2
3
4
5
6
7
8
Java

public class MyModel {

  @NumberFormat(style=Style.CURRENCY)
  private BigDecimal decimal;

}

6.6.2.1 포맷 어노테이션 API
포맷 어노테이션 API는 org.springframework.format.annotation 패키지에 있다. java.lang.Number 필드를 포매팅하려면 @NumberFormat를 사용하고 java.util.Date, java.util.Calendar, java.util.Long, Joda Time 필드를 포매팅하려면 @DateTimeFormat를 사용해라.

아래의 예제는 java.util.Date를 ISO Date (yyyy-MM-dd)로 포매팅하려고 @DateTimeFormat를 사용한다.


1
2
3
4
5
6
7
8
Java

public class MyModel {

  @DateTimeFormat(iso=ISO.DATE)
  private Date date;

}

6.6.3 FormatterRegistry SPI
FormatterRegistry 는 포매터와 컨버터를 등록하는 SPI다. FormattingConversionService는 대부분의 환경에 적합한 FormatterRegistry의 구현체이다. 이 구현체는 FormattingConversionServiceFactoryBean를 사용하는 스프링 빈처럼 프로그래밍적으로나 선언적으로 설정할 수 있다. 이 구현체가 ConversionService도 구현했기 때문에 스프링의 DataBinder와 스프링 표현언어(SpEL)를 사용해서 직접설정할 수도 있다. 아래의 FormatterRegistry SPI를 보자.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Java

package org.springframework.format;

public interface FormatterRegistry extends ConverterRegistry {

  void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser);

  void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter);

  void addFormatterForFieldType(Formatter<?> formatter);

  void addFormatterForAnnotation(AnnotationFormatterFactory<?, ?> factory);

}

위에서 보았듯이 Formatter는 fieldType이나 어노테이션으로 등록할 수 있다.

FormatterRegistry SPI는 여러 컨트롤러에서 중복된 설정을 하는 대신에 중앙에 포매팅 규칙을 설정할 수 있다. 예를 들어 모든 Date 필드를 특정 방법으로 포매팅하거나 특정 어노테이션을 가진 필드를 특정 방법으로 포매팅하는 것을 강제할 수 있다. 공유된 FormatterRegistry로 이러한 규칙을 한번만 정의하고 포매팅이 필요한 곳마다 적용한다.

6.6.4 FormatterRegistrar SPI
FormatterRegistrar는 FormatterRegistry를 통해 포매터와 컨버터를 등록하는 SPI다.


1
2
3
4
5
6
7
8
9
Java

package org.springframework.format;

public interface FormatterRegistrar {

  void registerFormatters(FormatterRegistry registry);

}

Date 포매팅처럼 주어진 포매팅 분류에 따라 관련된 여러가지 컨버터와 포매터를 등록할 때 FormatterRegistrar가 유용하다. 선언적인 등록이 충분하지 않을 때도 유용하다. 예를 들어 포매터가 포매터의 <T>와는 다른 특정 필드 타입하에 색인되어야 하거나 Printer/Parser 쌍을 등록하는 경우이다. 다음 섹션에서는 컨터버와 포매터 등록에 대해서 더 자세히 얘기한다.

6.6.5 Spring MVC에서의 포매팅 설정
스 프링 MVC 어플리케이션에서 MVC 네임스페이스의 annotation-driven 요소의 속성으로 커스텀 ConversionService 인스턴스를 명시적으로 설정할 수 있다. 컨트롤러 모델 바인딩을 하면서 타입변환이 필요할 때마다 이 ConversionService를 사용한다. 명시적으로 설정하지 않은 경우 스프링 MVC는 숫자나 날짜같은 일반적인 타입에 대한 기본 포매터와 컨버터를 자동으로 등록할 것이다.

기본 포매팅 룰을 사용하는데 스프링 MVC 설정 XML에서 어떤 커스턴 설정도 필요없다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:mvc="http://www.springframework.org/schema/mvc"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="
    http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
    http://www.springframework.org/schema/mvc
    http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">

  <mvc:annotation-driven/>

</beans>

이 한줄의 설정으로 @NumberFormat와 @DateTimeFormat 어노테이션을 포함해서 숫자와 날짜타임에 대한 기본 포매터를 설정할 수 있다. 클래스패스에 Joda Time 라이브러리가 있다면 Joda 타임 포매팅 라이브러리에 대한 완전한 지원을 할 수 있다.

커스텀 포매터와 컨버터가 등록된 ConversionService 인스턴스를 주입하려면 conversion-service 속성을 설정한 뒤 FormattingConversionServiceFactoryBean의 프로퍼티로 커스텀 컨버터나 포매터, FormatterRegistrar를 지정해라.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:mvc="http://www.springframework.org/schema/mvc"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="
    http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
    http://www.springframework.org/schema/mvc
    http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">

  <mvc:annotation-driven conversion-service="conversionService"/>

  <bean id="conversionService"
      class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <property name="converters">
      <set>
        <bean class="org.example.MyConverter"/>
      </set>
    </property>
    <property name="formatters">
      <set>
        <bean class="org.example.MyFormatter"/>
        <bean class="org.example.MyAnnotationFormatterFactory"/>
      </set>
    </property>
    <property name="formatterRegistrars">
      <set>
        <bean class="org.example.MyFormatterRegistrar"/>
      </set>
    </property>
  </bean>

</beans>

Note
FormatterRegistrar 사용에 대한 자세한 내용은 Section 6.6.4, “FormatterRegistrar SPI” 와 FormattingConversionServiceFactoryBean를 봐라.

6.7 Spring 3 유효성 검사(Validation)
스 프링 3에서는 유효성검사에 대한 지원이 여러 모로 강화되었다. 우선 JSR-303 Bean Validation API를 이제 완전히 지원한다. 두번째로 프로그래밍적으로 사용할 때 스프링의 DataBinder는 객체에 대한 바인딩 뿐만 아니라 객체의 유효성검사도 할 수 있다. 세번째로 스프링 MVC는 @Controller 입력에 대한 유효성 검사를 선언적으로 할 수 있다.

6.7.1 JSR-303 Bean Validation API의 개요
JSR- 303는 자바플랫폼의 유효성 검사 제약사항 선언과 메타데이터를 표준화한다. JSR-303 API를 사용해서 선언적인 유효성 제약사항으로 도에인 모델 프로퍼티에 어노테이션을 붙히고 런타임시에 이를 강제할 수 있다. 사용할 만한 다수의 내장 제약사항이 존재한다. 물론 자신만의 커스텀 제약사항도 정의할 수 있다.

설명을 위해 2개의 프로퍼티를 가진 간단한 PersonForm 모델을 생각해 보자.


1
2
3
4
5
6
Java

public class PersonForm {
  private String name;
  private int age;
}

JSR-303으로 이러한 프로퍼티에 대한 유효성 검사 제약사항을 선언적으로 정의할 수 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Java

public class PersonForm {

  @NotNull
  @Size(max=64)
  private String name;

  @Min(0)
  private int age;

}

JSR-303 Validator가 이 클래스의 인스턴스를 검사할 때 이러한 제약사항을 강제할 것이다.

JSR-303에 대한 일반적인 내용은 Bean Validation Specification를 봐라. 기본 레퍼런스 구현체의 특정 능력에 대한 내용은 Hibernate Validator 문서를 봐라. JSR-303 구현체를 어떻게 스프링 빈으로 설정하는지 알고 싶으면 계속 읽어봐라.

6.7.2 Bean Validation 구현체 설정
스 프링은 JSR-303 Bean Validation API를 완전히 지원한다. 이는 JSR-303 구현체를 스프링 빈으로 편리하게 설정하도록 하는 것도 포함한다. 또한 어플리케이션에서 유효성검사가 필요할 때마다 javax.validation.ValidatorFactory나 javax.validation.Validator를 주입할 수 있다.

기본 JSR-303 Validator를 스프링 빈으로 설정하려면 LocalValidatorFactoryBean를 사용해라.


1
2
3
Xml

<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>

위의 기본 설정은 JSR-303의 기본 부트스트랩 메카니즘을 사용해서 JSR-303을 초기화한다. Hibernate Validator같은 JSR-303 프로바이더는 클래스패스에 존재해야 하고 자동적으로 탐지될 것이다.

6.7.2.1 Validator 주입
LocalValidatorFactoryBean 는 org.springframework.validation.Validator뿐만 아니라 javax.validation.ValidatorFactory와 javax.validation.Validator를 모두 구현한다. 이러한 인터페이스에 대한 참조를 유효성검사 로직을 실행해야하는 빈에 주입할 것이다.

JSR-303 API를 직접 사용하는 걸 좋아한다면 javax.validation.Validator에 대한 참조를 주입해라.


1
2
3
4
5
6
7
8
9
Java

import javax.validation.Validator;

@Service
public class MyService {

  @Autowired
  private Validator validator;

빈(bean)이 Spring Validation API를 필요로 한다면 org.springframework.validation.Validator에 대한 참조를 주입해라.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Java

import org.springframework.validation.Validator;

@Service
public class MyService {

  @Autowired
  private Validator validator;

}

6.7.2.2 커스텀 제약사항(Constraints) 설정
각 JSR-303 유효성검사 제약사항은 두 부분으로 구성되어 있다. 첫째, 제약사항과 설정가능한 제약사항의 프로퍼티를 설정하는 @Constraint. 두번째는 제약사항의 동작을 구현하는 javax.validation.ConstraintValidator 인터페이스의 구현체이다. 선언과 구현체의 연결을 위해 각 @Constraint 어노테이션은 대응되는 ValidationConstraint 구현체를 참조한다. 런타임시에 ConstraintValidatorFactory는 제약사항 어노테이션이 도메인 모델을 만났을 때 참조된 구현체를 인스턴스화한다.

기본적으로 LocalValidatorFactoryBean는 스프링이 ConstraintValidator 인스턴스를 생성하려고 사용하는 SpringConstraintValidatorFactory를 설정한다. 이는 다른 스프링 빈처럼 의존성 주입의 이점을 가진 커스텀 ConstraintValidator를 사용할 수 있다.

다음은 의존성주입을 위해 스프링을 사용하는 관련 ConstraintValidator 구현체가는 붙은 커스텀 @Constraint 선언의 예제이다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
Java

@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=MyConstraintValidator.class)
public @interface MyConstraint {
}


Java

import javax.validation.ConstraintValidator;

public class MyConstraintValidator implements ConstraintValidator {

  @Autowired;
  private Foo aDependency;

  ...
}

여기서 보듯이 ConstraintValidator 구현체는 다른 스프링 빈처럼 @Autowired로 의존성을 가진다.

6.7.2.3 추가적인 설정 옵션
대 부분의 경우에 기본 LocalValidatorFactoryBean 설정으로도 충분하다. 메시지 삽입부터 탐색 처리(traversal resolution)까지 다양한 JSR-303 생성에 대한 다수의 설정 옵션이 있다. 이러한 옵션에 대한 자세한 내용은 LocalValidatorFactoryBean의 JavaDoc을 봐라.

6.7.3 DataBinder 설정
스 프링 3부터 DataBinder 인스턴스는 Validator와 함께 설정할 수 있다. 일단 설정되면 binder.validate() 호출에 의해서 Validator가 실행된다. 유효성 검사 오류는 자동적으로 바인더의 BindingResult에 추가된다.

프로그래밍적으로 DataBinder를 사용하는 경우 타겟 객체에 바인딩한 후 유효성 검사 로직을 실행하려고 DataBinder를 사용한다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Java

Foo target = new Foo();
DataBinder binder = new DataBinder(target);
binder.setValidator(new FooValidator());

// 타겟 객체에 바인딩
binder.bind(propertyValues);

// 타겟객체의 유효성 검사
binder.validate();

// 유효성검사 오류를 포함한 BindingResult 획득
BindingResult results = binder.getBindingResult();

6.7.4 Spring MVC 3 Validation
스프링 3부터 스프링 MVC는 @Controller의 입력을 자동으로 유효성감사할 수 있다. 이전 버전에서는 개발자가 수동으로 유효성검사 로직을 실행해야 했다.

6.7.4.1 @Controller 입력의 유효성 검사 실행
입력 아규먼트에 간단히 @Valid 어노테이션을 붙혀서 @Controller 입력에 대한 유효성검사를 실행한다.


1
2
3
4
5
6
7
Java

@Controller
public class MyController {

  @RequestMapping("/foo", method=RequestMethod.POST)
  public void processFoo(@Valid Foo foo) { /* ... */ }

적절한 Validator가 설정되었다면 Spring MVC 바인딩한 후 @Valid 객체의 유효성을 검사한다.


Note
@Valid 어노테이션은 표준 JSR-303 Bean Validation API의 일부이고 스프링에 한정된 것은 아니다.

6.7.4.2 Spring MVC가 사용하는 Validator 설정
@Valid 메서드 아규먼트가 있을때 호출되는 Validator 인스턴스는 2가지 방법으로 설정할 수 있다. 첫번째 방법은 @Controller의 @InitBinder 콜백내에서 binder.setValidator(Validator)를 호출하는 것이다. 이 방법으로 @Controller마다 Validator 인스턴스를 설정할 수 있다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Java

@Controller
public class MyController {

  @InitBinder
  protected void initBinder(WebDataBinder binder) {
   binder.setValidator(new FooValidator());
  }

  @RequestMapping("/foo", method=RequestMethod.POST)
  public void processFoo(@Valid Foo foo) { ... }

}

두번째 방법은 전역 WebBindingInitializer에서 setValidator(Validator)를 호출하는 것이다. 이 방법으로 모든 @Controllers에 걸쳐서 Validator 인스턴스를 설정할 수 있다. 이는 Spring MVC 네임스페이스로 쉽게 설정할 수 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:mvc="http://www.springframework.org/schema/mvc"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="
   http://www.springframework.org/schema/beans
   http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
   http://www.springframework.org/schema/mvc
   http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">

  <mvc:annotation-driven validator="globalValidator"/>

</beans>

6.7.4.3 Spring MVC가 사용하는 JSR-303 Validator 설정
JSR-303에서 단일 javax.validation.Validator 인스턴스는 보통 유효성검사 제약사항을 선언한 모든 모델 객체의 유효성을 검사한다. 스프링 MVC에 JSR-303에 기반한 Validator를 설정하려면 Hibernate Validator같은 JSR-303 Provider를 클래스패스에 추가한다. 스프링 MVC는 자동으로 이 프로바이더를 탐지해서 모든 컨트롤러에 걸쳐서 JSR-303 지원을 활성화할 것이다.

JSR-303 지원을 활성화하는데 필요한 스프링 MVC 설정은 다음과 같다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
Xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:mvc="http://www.springframework.org/schema/mvc"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="
   http://www.springframework.org/schema/beans
   http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
   http://www.springframework.org/schema/mvc
   http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">

  <!-- JSR-303 support will be detected on classpath and enabled automatically -->
  <mvc:annotation-driven/>

</beans>

이러한 최소의 설정으로 @Valid @Controller 입력을 만났을 때마다 JSR-303 프로바이더가 유효성을 검사한다. 다음으로 JSR-303은 입력에 대해 설정된 제약사항을 강제할 것이다. 모든 ConstraintViolation은 표준 스프링 MVC 폼태그로 랜더링할 수 있는 오류로 BindingResult에 노출된다.

[JAVA]6장 유효성검사(validation), 데이터 바인딩, 타입 변환 #1..

출처 : Outsider's Dev Story https://blog.outsider.ne.kr/

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

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 객체를 사용해서 동작한다.

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


1
2
3
4
5
6
7
8
9
Java

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 헬퍼 클래스를 알고 있다면 더욱 그렇다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
Java

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를 의존성 주입하거나 인스턴스화 해서 사용할 수 있다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
Java

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에 대한 섹션으로 건너뛰어도 좋다.)

다음 두 클래스를 보자.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
Java

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


Java

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의 프로퍼티를 어떻게 획득하고 조작하는 가를 보여주는 예제이다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
Java

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로 사용되려면 다음 클래스와 패키지 구조를 갖고 있어야 한다.



C-like

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


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



C-like

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


다음은 참조한 FooBeanInfo 클래스의 자바 소스코드다. Foo 클래스의 age 프로퍼티와 CustomNumberEditor를 연결한다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
Java

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 클래스를 고려해 봐라.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
Java

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 프로퍼티를 설정시에 문자열로 할당할 수 있기를 원한다.

1
2
3
4
5
Xml

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Java

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

public class ExoticTypeEditor extends PropertyEditorSupport {

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

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

1
2
3
4
5
6
7
8
9
Xml

<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 구현체를 생성해야 한다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Java

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에 주입한다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Xml

<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를 사용하는 예제다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Java

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에서 공유할 수 있다.