Convertor

FRAMEWORK/SPRING 2017. 10. 19. 17:12

13.Data Binding and Type Conversion

Spring에서 타입 변환이 발생하는 영역은 크게 2가지이다. 하나는 Bean 정의 XML에서 <property />를 이용해 설정한 값을 실제 Bean 객체의 Property에 바인딩 시킬 때인데, XML에 String으로 정의한 값을 해당 Property의 타입으로 변환해서 셋팅해야한다.

예를 들어, Movie 클래스가 다음과 같이 정의되어 있고,

public class Movie {
    String id;
    String name;
    int ticketPrice;
}

'movie' Bean을 아래와 같이 정의했다고 하면,

<bean id="movie" class="sample.Movie">
    <property name="name" value="Avatar"/>
    <property name="ticketPrice" value="7500"/>
</bean>

'name'이라는 Property는 같은 String 타입이기 때문에 문제가 없지만, 'ticketPrice'의 경우 String으로 작성된 '7500'값을 int 타입의 7500으로 변환하여 바인딩 해야한다.

타입 변환이 발생하는 다른 한가지 경우는, 아래 코드 예와 같이 HTTP Request 파라미터로 들어온 사용자 입력 값들을 'Movie'라는 Model 객체에 바인딩시킬 때이다. 여기서도 마찬가지로 문자열로 표현된 값을 특정 타입으로 변환하는 과정이 필요하다.

@RequestMapping("/movies/new", method=RequestMethod.POST)
public String create(@ModelAttribute Movie movie, BindingResult results) {

    this.movieService.create(movie);
    status.setComplete();

    return "redirect:/movies";
}

또한 단순히 타입의 변환이 아니라, 사용자가 보는 View에서 값에 "$45.22"와 같은 특정 Format이 적용되어 변환되어야 하는 경우도 종종 있다.

이 장에서는 이러한 타입 변환을 위해서 Spring에서 지원하고 있는 기술들에 대해서 자세히 알아보도록 하겠다.

13.1.PropertyEditor

Spring에서는 위에서 언급한 타입 변환을 위해서 기본적으로 JavaBeans 표준에서 제공하는 PropertyEditor를 사용해왔다. PropertyEditor는 String과 특정 타입 객체 간의 변환 로직을 구현할 수 있는 인터페이스이다.

13.1.1.Implementing Custom Editor

타입 변환시 호출되는 PropertyEditor의 메소드는 setValue()/getValue(), setAsText()/getAsText() 4가지 이다. PropertyEditorSupport를 상속받아서 setAsText()/getAsText() 메소드만 오버라이드하면 특정 타입 변환을 위한 PropertyEditor를 구현할 수 있다.

Spring에서 제공하고 있는 CustomBooleanEditor 코드를 조금 살펴보면, 아래와 같이 setAsText() 메소드에는 String값을 받아서 boolean값으로 변환하여 setValue() 해주는 로직이 구현되어 있고, getAsText() 메소드에는 getValue() 호출해서 가져온 값을 String으로 변환하여 리턴하는 로직이 구현되어 있다.

@Override
public void setAsText(String text) throws IllegalArgumentException {
    String input = (text != null ? text.trim() : null);
    if (this.allowEmpty && !StringUtils.hasLength(input)) {
        setValue(null);
    } else if (this.trueString != null && input.equalsIgnoreCase(this.trueString)) {
        setValue(Boolean.TRUE);
    } else if (this.falseString != null && input.equalsIgnoreCase(this.falseString)) {
        setValue(Boolean.FALSE);
    // 중략
    } else {
        throw new IllegalArgumentException("Invalid boolean value [" + text + "]");
    }
}

@Override
public String getAsText() {
    if (Boolean.TRUE.equals(getValue())) {
        return (this.trueString != null ? this.trueString : VALUE_TRUE);
    } else if (Boolean.FALSE.equals(getValue())) {
        return (this.falseString != null ? this.falseString : VALUE_FALSE);
    } else {
        return "";
    }
}

13.1.2.Default PropertyEditors

위에서 본 CustomBooleanEditor와 같이 Spring에서는 기본 타입에 대해서 이미 구현해놓은 여러가지 Build-in PropertyEditor들을 제공한다. Built-in PropertyEditor들은 모두 org.springframework.beans.propertyeditors 패키지 하위에 존재한다.

ClassEditor, FileEditor, InputStreamEditor, LocaleEditor, PropertiesEditor 등의 Built-in PropertyEditor들의 이름에서 볼 수 있듯이 Built-in PropertyEditor들은 변환할 타입에 'Editor'라는 이름을 붙인 클래스들이다. CustomNumberEditorr와 같이 사용자가 Customizing이 가능한 PropertyEditor에는 'Custom'이라는 접두어가 붙기도 한다. 이들은 모두 디폴트로 등록되어 내부적으로 사용되지만, CustomDateEditor와 StringTrimmerEditor는 디폴트로 등록되지 않기 때문에, 사용이 필요한 경우에는 반드시 직접 코드에서 등록해 주어야 한다.

13.1.3.Register Custom Editor

기본적으로 Spring에서는 Built-in PropertyEditor들을 미리 등록해놓고 사용하고 있다. 이외에 추가로 Custom Editor 등록이 필요한 경우 따로 등록을 해주어야 하는데, 이 장에서는 Custom PropertyEditor를 어떻게 등록할 수 있는 지에 대해서 알아보도록 하겠다. Spring MVC에서 사용자가 추가로 개발한 Custom PropertyEditor를 등록하는 방법에는 아래와 같이 3가지가 있다.

  • 개별 컨트롤러에 적용

    Controller에서 @InitBinder annotation을 이용하여 PropertyEditor 등록하는 메소드 정의

    @InitBinder
    public void initBinder(WebDataBinder binder) { 
        DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
        binder.registerCustomEditor(Date.class, new CustomDateEditor(df, false));
    }
  • 전체 컨트롤러에 적용

    어플리케이션 전반에서 많이 사용되는 Custom PropertyEditor의 경우 WebBindingInitializer 이용

    1. WebBindingInitializer를 구현한 클래스 생성

      public class ClinicBindingInitializer implements WebBindingInitializer {
          @Autowired
          private Clinic clinic;
      
          public void initBinder(WebDataBinder binder, WebRequest request) {
              SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
              dateFormat.setLenient(false);
              binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
              binder.registerCustomEditor(String.class, new StringTrimmerEditor(false));
              binder.registerCustomEditor(PetType.class, new PetTypeEditor(this.clinic));
          }
      }
    2. AnnotationMethodHandlerAdapter에 webBindingInitializer 속성을 이용해서 설정

      <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
          <property name="webBindingInitializer">
              <bean class="org.springframework.samples.petclinic.web.ClinicBindingInitializer" />
          </property>
      </bean>
  • 여러 개의 PropertyEditor를 여러 컨트롤러에 적용

    다수의 컨트롤러에서 자주 사용되는 여러 개의 Custom PropertyEditor 셋트로 관리할 경우 PropertyEditorRegistrar 이용

    1. PropertyEditorRegistrars를 구현한 클래스 생성

      package com.foo.editors.spring;
      					
      public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar {
          public void registerCustomEditors(PropertyEditorRegistry registry) {
      
              // 새로운 PropertyEditor 인스턴스 생성
              registry.registerCustomEditor(ExoticType.class, new ExoticTypeEditor());
      
              // 필요한 Custom PropertyEditor들 추가
          }
      }
    2. 구현한 Custom PropertyEditorRegistrar를 Bean으로 등록

      <bean id="customPropertyEditorRegistrar" class="com.foo.editors.spring.CustomPropertyEditorRegistrar"/>
    3. @InitBinder를 이용하여 Controller에서 사용

      @Inject
      private final PropertyEditorRegistrar customPropertyEditorRegistrar;
      					
      @InitBinder
      public void initBinder(WebDataBinder binder) { 
          this.customPropertyEditorRegistrar.registerCustomEditors(binder);
      }

13.1.4.PropertyEditor의 단점

PropertyEditor는 기본적으로 String과 특정 타입 간의 변환을 지원한다. PropertyEditor는 변환 과정 중에, 변환하려고 하는 Object나 String을 PropertyEditor 객체에 잠깐 저장하였다가 변환하기 때문에, 여러 Thread에서 동시에 사용하는 경우, 변환 도중에 가지고 있던 값이 변경되어 엉뚱한 변환 값을 전달할 수도 있다. 이런 이유에서 PropertyEditor는 Thread-Safe하지 않기 때문에, Sington Bean으로 사용하지 못하고 위에서 봤던 예제 코드에서 처럼 항상 'new'를 통해서 새로 생성해야 한다.

13.2.Spring 3 Type Conversion

앞서 언급했듯이 JavaBeans의 표준인 PropertyEditor에는 몇가지 단점이 존재한다. 또한 Spring 내부적으로도 한쪽이 String으로 제한된 타입 변환이 아니라 좀 더 일반적인 타입 변환이 요구되기 시작했다. 그래서 Spring 3에서는 PropertyEditor의 단점을 극복하고 내부적으로 타입 변환이 일어나는 모든 곳에서 사용할 수 있는 범용적인 Type Conversion System을 내놓았다. 이와 관련된 클래스들은 모두 org.springframework.core.convert 패키지 하위에 존재한다. 이 장에서는 Spring 3에서 소개한 Type Conversion 서비스의 사용방법에 대해서 자세히 알아보도록 하겠다.

13.2.1.Implementing Conveter

Spring 3에서는 Converter 구현을 위해서 다음과 같이 여러가지 API를 제공하고 있다.

  • Converter

    Spring 3 Type Conversion 시스템에서 타입 변환을 실제 담당하는 객체는 Converter이다. Converter를 작성하려면 Spring에서 제공하는 org.springframework.core.convert.converter.Converter<S, T> 인터페이스를 구현하면 된다. Generics를 이용해서 Converter를 정의하므로 Run-time Type-Safety를 보장해준다.

    package org.springframework.core.convert.converter;
    				
    public interface Converter<S, T> {
        T convert(S source);    
    }

    Converter 인터페이스에서 구현해야 할 메소드는 convert() 메소드 하나이다. 즉 PropertyEditor와는 달리 단방향 타입 변환만 제공한다. 'S'에는 변환 전인 Source 타입을 명시하고, 'T'에는 변환 할 Target 타입을 명시한다. Converter 객체가 변환과 관련된 상태 값을 저장하지 않기 때문에 Converter를 Singlton Bean으로 등록하여 Multi-thread 환경에서도 안전하게 사용할 수 있다.

    다음은 Converter를 구현한 예제 코드이다.

    final class StringToInteger implements Converter<String, Integer> {
    
        public Integer convert(String source) {
            return Integer.valueOf(source);
        }   
    }
  • ConverterFactory

    클래스 계층으로 묶을 수 있는 java.lang.Number나 java.lang.Enum과 같은 타입 변환 로직을 한 곳에서 관리하고자 하는 경우, 아래의 ConverterFactory 인터페이스의 구현클래스를 작성하면 된다..

    package org.springframework.core.convert.converter;
    				
    public interface ConverterFactory<S, R> {
        <T extends R> Converter<S, T> getConverter(Class<T> targetType);
    }
    여기서 'S'에는 변환 전인 Source 타입을 명시하고, 'R'에는 변환할 클래스들의 상위 베이스 클래스를 명시한다. 그리고 getConverter() 메소드를 구현하는데, 이 때, 'T'는 'R'의 하위 클래스 타입이 될 것이다.

    다음은 ConverterFactory의 구현클래스 예이다. (Spring에서 제공하는 StringToNumberConverterFactory이다.)

    final class StringToNumberConverterFactory implements ConverterFactory<String, Number> {
    
        public <T extends Number> Converter<String, T> getConverter(Class<T> targetType) {
            return new StringToNumber<T>(targetType);
        }
    
        private static final class StringToNumber<T extends Number> implements Converter<String, T> {
    
            private final Class<T> targetType;
    
            public StringToNumber(Class<T> targetType) {
                this.targetType = targetType;
            }
    
            public T convert(String source) {
                if (source.length() == 0) {
                    return null;
                }
                return NumberUtils.parseNumber(source, this.targetType);
            }
        }
    }
  • GenericConverter

    또한, 두 가지 이상의 타입 변환을 수행하는 Converter를 개발하고자 하는 경우에는 GenericConverter 인터페이스를 구현하면 된다. 여러개의 Source/Target 타입을 지정할 수 있고, Source나 Target 객체의 Field Context(Field에 적용된 Annotation이나 Generics 등을 포함한 Field와 관련된 모든 정보)를 사용할 수 있기 때문에 유연한 Converter이긴 하지만, 그만큼 구현하기가 어렵고 복잡하다. 일반적으로 Converter나 ConverterFactory만으로 커버할 수 있는 기본적인 변환에는 사용하지 않는 것이 좋다.

    package org.springframework.core.convert.converter;
    					
    public interface GenericConverter {
    
        public Set<ConvertiblePair> getConvertibleTypes();
        
        Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
    }
    실제 GenericConverter 구현 모습을 보고 싶다면, Spring에서 제공하는 Built-in Converter 중 하나인 org.springframework.core.convert.support.ArrayToCollectionConverter 코드에서 확인할 수 있다.

  • ConditionalGenericConverter

    만약 어떤 조건을 만족하는 경우에만 변환을 수행하는 Converter를 개발할 경우는 ConditionalGenericConverter 인터페이스 구현클래스를 작성한다. 참조할 수 있는 구현 예는 Spring의 org.springframework.core.convert.support.IdToEntityConverter 이다.

13.2.2.Default Converter

Spring에서는 Converter도 PropertyEditor처럼 기본적인 타입들에 대해서 이미 구현해놓은 Built-in Converter들을 제공한다. Built-in Converter들은 모두 org.springframework.core.convert.support 패키지 하위에 존재한다.

13.2.3.Register Converter

사용자 필요에 의해서 추가로 개발한 Custom Converter들을 사용하려면 Converter도 역시 PropertyEditor처럼 등록이 필요하다. 한가지 다른 점은 각각의 Converter를 개별적으로 등록하는 것이 아니라, 모든 Converter를 가지고 변환 작업을 처리하는 ConversionService를 Bean으로 등록한 후, ConversionService Bean을 필요한 곳에서 Inject 받아서 사용한다는 것이다.

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

실제 Run-time시에 Converter들의 변환 로직은 이 ConversionService에 의해서 실행된다. 기본적으로 Spring에서 사용되는 ConversionService 구현 클래스는 GenericConversionService이다. 대부분의 ConversionService 구현 클래스는 Converter 등록 기능을 가지고 있는 ConverterRegistry도 구현하고 있다.

  • ConversionService Bean 정의 시 'converters' 속성 이용

    ConversionService 구현클래스인 GenericConversionService는 ConversionServiceFactoryBean을 이용해서 Bean으로 등록할 수 있다. ConversionServiceFactoryBean이 가진 'converters' 속성을 이용하면 Custom Converter를 추가할 수도 있다.

    다음은 ConversionServiceFactoryBean을 사용하여 ConversionService를 Bean으로 정의한 모습이다.

    <bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
        <!-- 추가할 Custom Converter를 설정 -->
        <property name="converters">
            <list>
                <bean class="org.anyframe.sample.moviefinder.StringToFilmRatingConverter" />
                <bean class="org.anyframe.sample.moviefinder.FilmRatingToStringConverter" />
            </list>
        </property>					
    </bean>
    ConversionServiceFactoryBean은 ConversionServiceFactory 클래스를 이용해서 디폴트 Converter들을 GenericConversionService에 등록하고, 'converters' 속성을 통해 추가된 Converter들을 등록한다.

    'conversionService'이라는 Bean 이름은 Spring에게 양보!

    Spring 3에서는 타입 변환을 위해 Run-time 시에 사용되는 ConversionService Bean을 'conversionService'라는 이름으로 찾는다. 따라서 다른 용도의 Bean을 'conversionService'라는 이름으로 등록해서는 안된다.

13.2.4.ConversionService 사용하기

앞서 PropertyEditor는 매번 new 키워드를 이용해서 매번 인스턴스를 새로 생성해야만 했기 때문에 개별 컨트롤러 적용방법과 전체 컨트롤러 적용방법이 달랐었지만, Converter의 경우는 모든 Converter들을 가지고 있는 ConversionService를 Singleton Bean으로 등록해서 사용하기 때문에 아래와 같이 개별 컨트롤러에서 사용하는 것과, WebBindingInitializer 구현클래스를 이용해서 전체 컨트롤러에서 적용하는 것이 차이가 없다.

@Inject
private ConversionService conversionService;

@InitBinder
public void initBinder(WebDataBinder binder) {
    binder.setConversionService(this.conversionService);
}

따라서 WebBindingInitializer를 구현한 클래스를 이용하여 하나의 설정으로 등록하는 것이 편리하다. Spring에서는 WebBindingInitializer를 직접 구현하지 않고 선언적인 설정만으로도 WebDataBinder의 설정을 초기화할 수 있게 해주는 ConfigurableWebBindingInitializer를 제공한다.

아래와 같이 설정하기만 하면 Custom Converter들이 추가된 ConversionService가 타입 변환 시에 사용될 것이다.

<!-- AnnotationMethodHandlerAdapter에 webBindingInitializer DI -->
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
    <property name="webBindingInitializer" ref="webBindingInitializer" />
</bean>

<!-- 사용자가 변경한 conversionService를 WebBindingInitializer 구현체에 DI -->
<bean id="webBindingInitializer" class="org.springframework.web.bind.support.ConfigurableWebBindingInitializer">
    <property name="conversionService" ref="conversionService" />
</bean>

<!-- Custom Converter들을 추가한 conversionService Bean 정의 -->
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <list>
            <bean class="org.anyframe.sample.moviefinder.StringToFilmRatingConverter" />
        	<bean class="org.anyframe.sample.moviefinder.FilmRatingToStringConverter" />
        </list>
    </property>
</bean>

위와 같은 복잡한 설정을 쉽고 간편하게 할 수 있도록 Spring 3에서는 mvc 네임스페이스를 제공한다.

<mvc:annotation-driven>에 대한 자세한 내용은 본 매뉴얼 Spring MVC >> Configuration에서 Configuration Simplification 내용을 참고하기 바란다.

13.3.Spring 3 Formatting

지금까지 설명한 Conversion System은 Spring에서 범용적인 사용을 목적으로 만들어졌다. Spring 컨테이너에서 Bean의 Property 값을 셋팅할 때, Controller에서 데이터를 바인딩할 때는 물론이고 SpEL에서 데이터 바인딩 시에도 이 Conversion System을 사용한다.

Conversion System은 하나의 타입에서 다른 타입으로의 변환 로직을 구현할 수 있는 일관성있는 API를 제공한다. 그러나 실제로 사용자 UI가 존재하는 어플리케이션에서는 단순한 타입 변환만이 아니라, 날짜나 통화 표현같이 특정 Format을 객체의 값에 적용하여 String으로 변환해야 하는 경우가 종종 있다. 범용적인 용도로 만들어진 Converter에는 이러한 Formatting에 대한 처리 방법이 명시되어있지 않다.

그래서 Spring 3에서는 다음과 같은 Formatter API를 제공한다.

package org.springframework.format;

public interface Formatter<T> extends Printer<T>, Parser<T> {
}
public interface Printer<T> {
    String print(T fieldValue, Locale locale);
}
import java.text.ParseException;

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

13.3.1.Implementing Formatter

Formatter를 개발하기 위해서는 위의 Formatter 인터페이스를 구현하여야 한다. print() 메소드에서 format을 적용하여 출력하는 로직을 구현하고, parse() 메소드에는 format이 적용된 String 값을 분석해서 객체 인스턴스로 변환하는 로직을 구현하면 된다. 위의 인터페이스 정의에서 볼 수 있듯이, Locale 정보도 함께 넘겨주기 때문에 Localization 적용도 쉽게 처리할 수 있다.

다음은 구현된 Formatter 예제 코드이다.

public final class DateFormatter implements Formtter<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;
    }
}

13.3.2.Default Formatter

Spring에서는 편의를 위해서 Formatter 역시 기본적인 Built-in Formatter를 제공하고 있다.

  • DateFormatter

    Spring은 기본적으로 java.text.DateFormat을 가지고 java.util.Date 객체의 formatting 처리를 하는 DateFormatter를 제공한다. (org.springframework.format.datetime 패키지) 또한 Spring에서는 강력한 Date/Time 관련 기능을 지원하는 Joda Time Library를 이용한 formatting도 제공한다.(org.springframework.format.datetime.joda 패키지) 클래스패스상에 Joda Time Library가 존재한다면 디폴트로 동작한다.

  • NumberFormatter

    Spring에서는 java.text.NumberFormat을 사용한 java.lang.Number 객체의 formatting처리를 위해서 NumberFormatter, CurrencyFormatter, PercentFormatter를 제공하고 있다.(org.springframework.format.number 패키지)

일반적으로는 위의 Formatter를 직접 사용하기 보다는 아래에서 살펴볼 Annotation 기반 Formatting 처리 방법, 특히 Spring에서 기본적으로 제공하는 Formatting 관련 Annotation 들을 주로 사용하게 될 것이다.

13.3.3.Annotation 기반 Formatting

다음 섹션에서 살펴보겠지만, 구현된 Formatter는 특정 타입의 변환 시에 사용되도록 등록할 수도 있지만, 특정 Annotation이 적용된 필드의 타입 변환 시에 사용되도록 등록할 수도 있다.

  • Implementation

    Formatting 관련 Annotation을 정의하고 그 Annotation이 적용된 필드의 타입 변환에는 연결되어 있는 특정 Formatter가 사용되도록 하려면 필드에 사용할 Annotation과 AnnotationFormatterFacotry 구현체를 만들어야 한다.

    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'에는 연결할 Annotation을 명시하고, getFieldTypes()은 해당 Annotation을 적용할 수 있는 필드 타입을 리턴하도록 구현하고, getPrinter()/getParser()는 각각 사용될 Printer와 Parser를 리턴하도록 구현한다.

    실제로 Spring에서 제공하고 있는 @NumberFormat의 경우 Annotation과 AnnotationFormatterFacotry가 어떻게 구현되어 있는지 살펴보자.

    다음은 @NumberFormat Annotation 구현 코드이다.

    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface NumberFormat {
    
        Style style() default Style.NUMBER;
    
        String pattern() default "";
    
        public enum Style {
            NUMBER,
            CURRENCY,
            PERCENT
        }
    }

    그리고 다음 코드는 @NumberFormat이 적용된 필드에 어떤 Formatter가 사용되어야 하는지 연결한 AnnotationFormatterFacotry 구현체이다.

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

    이렇게 구현한 Formatter가 실제 Run-time 타입 변환 시에 사용되려면 반드시 등록과정을 거쳐야 한다. Formatter 등록에 대해서는 다음 섹션에서 자세히 알아보도록 하자.

  • Default annotations

    Spring에서 제공하는 Format 관련 Annotation은 아래와 같이 2가지가 있다.

    • @DateTimeFormat : java.util.Date, java.util.Calendar, java.util.Long, Joda Time 타입(LocalDate, LocalTime, LocalDateTime, DateTime)의 필드 formatting에 사용 가능

      public class Movie {
          // 중략
          @DateTimeFormat(pattern="yyyy-MM-dd")
          private Date releaseDate;
      }
      위와 같이 필드에 @DateTimeFormat을 적용하기만 하면 @DateTimeFormat에 연결된 Formatter에 의해서 Formatting이 처리된다.

      사용 가능한 속성은 다음과 같다.

      NameDescription
      style

      'S'-Short, 'M'-Medium, 'L'-Long, 'F'-Full 4가지 문자를 날짜에 한글자, 시간에 한글자를 사용해서 두 개의 문자로 만들어 지정. 날짜나 시간을 생략하고자 하는 경우 '-'를 사용 (예: 'S-'). 디폴트 값은 'SS'. Locale 정보를 기반으로 적절한 표현 형식을 적용해 줌

      isoISO 표준을 사용하고자 하는 경우, @DateTimeFormat(iso=ISO.DATE)와 같이 지정. ISO.DATE, ISO.DATE_TIME, ISO.TIME, ISO.NONE 사용가능, Locale 정보를 기반으로 적절한 표현 형식을 적용해 줌
      patternLocale과 상관없이 임의의 패턴을 사용하고자 하는 경우, ‘yyyy/mm/dd h:mm:ss a’등의 패턴을 지정

    • @NumberFormat : java.lang.Number 타입의 필드 formatting에 사용 가능

      public class Movie {
          // 중략
          @NumberFormat(pattern = "#,##0")
          private int ticketPrice;
      }
      위와 같이 필드에 @NumberFormat을 적용하기만 하면 @NumberFormat에 연결된 Formatter에 의해서 Formatting이 처리된다. java.lang.Number 하위의 클래스인 Byte, Double, Float, Integer, Long, Short, BigInteger, BigDecimal 변환에도 사용할 수 있다.

      사용 가능한 속성은 다음과 같다.

      NameDescription
      style

      NUMBER, CURRENCY, PERCENT 중 선택 가능. Locale 정보를 기반으로 적절한 표현 형식을 적용해 줌

      patternLocale과 상관없이 임의의 패턴을 사용하고자 하는 경우, ‘#,##0’등의 패턴을 지정

13.3.4.Register Formatter

Converter 영역에서, 등록된 Converter들을 가지고 실제 Run-time시에 타입 변환을 처리하는 역할을 담당하는 것이 GenericConversionService라면, Formatter에서 GenericConversionService와 같은 역할을 담당하는 것은 FormattingConversionService이다. FormattingConversionService는 GenericConversionService를 상속받고 있다.

위에서 살펴본 과정을 통해서 구현한 Formatter를 등록하는 방법은 Converter 등록과는 달리 불편하다. 설정으로 등록할 수 있는 방법은 아직 제공하고 있지 않고, FormattingConversionService를 초기화해주는 FormattingConversionServiceFactoryBean을 상속받은 클래스를 만들어서, installFormatters() 메소드를 오버라이드하여 Custom Formatter를 추가해야한다.

public class CustomFormattingConversionServiceFactoryBean extends FormattingConversionServiceFactoryBean {

    @Override
    protected void installFormatters(FormatterRegistry registry) {
        super.installFormatters(registry);
        
        // 필드 타입과 Formatter를 연결하여 등록하는 경우
        registry.addFormatterForFieldType(FilmRatings.class, new FilmRatingsFormatter());
        
        // Annotation과 Formatter를 연결하여 등록하는 경우
        registry.addFormatterForFieldAnnotation(new FilmRatingsFormatAnnotationFormatterFactory());
    }
}
위 코드에서 FormatterRegistry가 Formatter 등록과 관련된 메소드를 제공하는 것을 확인할 수 있다.

이렇게 확장한 FormattingConversionServiceFactoryBean를 아래와 같이 Bean으로 등록하고, Converter에서처럼 ConfigurableWebBindingInitializer를 이용하여 컨트롤러에서 사용할 수 있도록 설정할 수도 있고,

<bean id="conversionService" class="org.anyframe.sample.format.CustomFormattingConversionServiceFactoryBean" />
아래와 같이 mvc 네임스페이스의 <mvc:annotation-driven>를 이용하면 간편하게 설정할 수도 있다.
<mvc:annotation-driven conversion-service="conversionService" />
    
<bean id="conversionService" class="org.anyframe.sample.format.CustomFormattingConversionServiceFactoryBean" />

<mvc:annotation-driven>만 설정해주면 기본적으로 제공하는 Built-in Converter와 Built-in Formatter, 그리고 Formatting관련 Annotation인 @DateTimeFormat, @NumberFormat을 사용할 수 있다.

PropertyEditor와 Spring 3 Converter 간의 실행 순서

타입변환이 필요한 경우 기본적으로 ConversionService가 등록되지 않으면 Spring은 PropertyEditor를 기반으로 타입 변환을 수행한다. ConversionService가 등록된 경우라고 하더라도 Custom PropertyEditor가 등록된 경우는 Custom PropertyEditor가 우선적으로 적용된다. Even when ConversionService has been registered, Custom PropertyEditor takes priority when Custom PropertyEditor is registered.

* 우선 순위

  1. Custom PropertyEditor

  2. Converter

  3. Default PropertyEditor

출처 - http://dev.anyframejava.org/docs/anyframe/plugin/essential/core/1.0.0/reference/html/ch13.html

: