본문 바로가기
  • 실행력이 모든걸 결정한다
Spring Series/Spring Framework

[Spring] 프로퍼티 바인딩 방식(2) - Converter와 Formatter

by 김코더 김주역 2022. 9. 21.
반응형

1. Converter

1) Converter 소개

- Spring 3.0에서 PropertyEditor를 대신하기 위해 도입된 타입 변환 API다. 매번 바인딩을 할 때마다 새로운 오브젝트가 생성되는 것이 싫거나, prototype 스코프를 가지는 bean을 사용하기 불편한 경우에 적합하다.

- Converter를 사용하면 바인딩 과정에서 메소드가 한 번만 호출되며, 상태를 인스턴스 변수로 저장하지 않는다. 따라서, 멀티스레드 환경에서도 singleton 스코프를 가지는 일반적인 bean을 사용할 수 있다.

- PropertyEditor와 다르게 한 쪽 타입이 String으로 고정되어 있지 않고, 양 쪽 타입 둘 다 임의로 지정할 수 있다. 단, 소스 타입에서 타깃 타입으로의 단방향 변환만 지원한다.

 

 

2) Converter 인터페이스

- Converter은 인터페이스이기 때문에 구현체를 만들어서 사용해야 한다. convert() 메소드를 구현하면 된다.

public interface Converter<S, T> {
    T convert(S source);
}

 

- 이전 포스팅에서 사용했던 LevelPropertyEditor처럼 String 타입의 텍스트를 Level Enum 타입으로 변환하는 기능을 가진 Converter 구현체를 생성했다.

public class StringToLevelConverter implements Converter<String, Level> {
    public Level convert(String text) {
        return Level.valueOf(Integer.parseInt(text));
    }
}

역방향 변환도 필요하다면 Converter 구현체를 하나 더 생성해서 같이 사용하면 된다.

 

 

3) 그 외의 타입 변환 오브젝트

- Spring 3.0에 추가된 타입 변환 오브젝트는 Converter 외에도 GenericConverter, ConverterFactory가 있다.

  • GenericConverter : 필드 컨텍스트라는 메타정보를 활용하여 타입 변환 로직을 작성할 수 있다.
  • ConverterFactory : 제네릭스를 활용하여 특정 타입에 대한 컨버터 오브젝트를 만들어주는 팩토리를 구현할 때 사용된다.

 

 

 

2. ConversionService

1) ConversionService 소개

- 여러 종류의 컨버터를 이용해서 하나 이상의 타입 변환 서비스를 제공해주는 오브젝트를 만들 때 사용되는 인터페이스다. 그래서 bean으로 등록되어 모든 바인딩 작업에 적용되더라도 성능에 부담이 없는 편이다.

- 컨트롤러의 바인딩 작업에 Converter를 적용하려면 ConversionService를 통해 WebDataBinder에 설정해줘야 한다. 

 

 

2) GenericConversionService

- Spring이 제공하는 ConversionService의 구현체로, ConversionServiceFactoryBean에서 자동으로 생성된다.

- Spring의 다양한 타입 변환 기능을 가진 오브젝트를 등록하는 ConverterRegistry 인터페이스도 구현되어 있다.

 

 

3) Converter 추가 방법

- GenericConversionService에 컨버터를 추가하는 방법들을 소개한다.

 

(1) GenericConversionService.addConverter() 메소드

 

(2) GenericConversionService.addConverterFactory() 메소드

- 만약 xml 설정 방식을 이용한다면 다음과 같이 ConversionSerivceFactoryBean bean의 converter 프로퍼티에 컨버터들을 등록해주면 된다.

<bean class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <set>
            <bean class="com.example.demo.converter.StringToLevelConverter" />
            <bean class="com.example.demo.converter.LevelToStringConverter" />
        </set>
    </property>
</bean>

※ 내부 빈 방식을 사용하고 싶지 않다면 <ref bean="" />으로 추가하면 된다.

 

 

4) ConversionService 적용 방법

- 컨버전 서비스를 컨트롤러에 적용하는 방법들을 소개한다.

 

(1) @InitBinder

- 일부 컨트롤러에만 적용하고 싶을 때 사용된다.

- WebDataBinder.setConversionService() 메소드를 통해 ConversionService를 등록하면 된다. WebDataBinder에는 하나의 ConversionService 타입 오브젝트만 허용한다.

@AutoWired ConversionService conversionSerivce;

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

 

(2) ConfigurableWebBindingInitializer

- 모든 컨트롤러에 일괄적으로 적용하고 싶을 때 사용된다.

- WebBindingInitializer을 직접 이용하는 방법도 있지만, ConfigurableWebBindingInitializer를 이용하여 bean 설정만으로 WebBindingInitializer bean을 AnnotationMethodHandlerAdapter에 등록할 수도 있다.

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

<bean id="webBindingInitializer" class="org.springframework.web.bind.support.ConfigurableWebBindingInitializer" >
	<property name="conversionService" ref="conversionService" />
</bean>

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

 

 

 

 

3. Formatter와 FormattingConversionService

1) Formatter와 FormattingConversionService 소개

- Formatter를 사용하면 String 타입의 요청 파라미터와 컨트롤러 메소드의 파라미터 사이에서 양방향으로 타입을 변환할 수 있고, FormattingConversionService를 통해 Formatter를 GenericConverter 타입으로 포장해서 컨버전 서비스에 등록할 수 있다.

- 모델 오브젝트의 내용을 뷰에 출력할 때 오브젝트 타입이 문자열 타입으로 변환된다.

 

 

2) Formatter 인터페이스

- Formatter 인터페이스는 문자열을 오브젝트로 변환해주는 parse() 메소드와 오브젝트를 문자열로 변환해주는 print() 메소드를 가진다.

- Formatter의 메소드의 파라미터에는 Locale도 함께 제공되기 때문에 지역 정보를 참고해서 변환이 가능하다.

public interface Formatter<T> extends Printer<T>, Parser<T> {
	String print(T object, Locale locale);
    T parse(String text, Locale locale) throws ParseException;
}

 

- AnnotationFormatterFactory 인터페이스를 사용하면 지역 정보뿐만 아니라 오브젝트 필드의 어노테이션 정보를 활용해서 Formatter를 만들 수 있다. AnnotationFormatterFactory 타입 오브젝트는 FormattingConversionService를 통해 ConditionalGenericConverter로 포장된다.

 

 

3) FormattingConversionServiceFactoryBean

- 직접 만든 포맷터를 적용하려면 FormattingConversionService를 초기화해주는 FormattingConversionServiceFactoryBean을 상속해서, Formatter를 초기화해주는 installFormatters() 메소드를 오버라이딩하면 된다.

 FormattingConversionServiceFactoryBean 문서 : https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/format/support/FormattingConversionServiceFactoryBean.html

- FormattingConversionServiceFactoryBean은 다음과 같이 bean으로 등록할 수 있으며, 이렇게 만들어진 컨버전 서비스를 @InitBinder 또는 ConfigurableWebBindingInitializer를 통해 컨트롤러에 적용하면 된다.

<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean" />

- FormattingConversionServiceFactoryBean을 사용할 때 자동으로 등록해주는 포맷터에는 @NumberFormat과 @DateTimeFormat이 있다. 이 2개의 포맷터들은 모두 도메인 오브젝트의 필드에 어노테이션으로 지정한 메타 정보를 활용해서 세밀한 변환 조건을 적용할 수 있다.

 

(1) @NumberFormat

- 문자열과 java.lang.Number 타입의 오브젝트를 상호 변환해준다. Number 타입 오브젝트로는 Byte, Float, Double, Integer, Long, Short, BigInteger, BigDecimal이 있다.

- @NumberFormat의 style 속성으로 Style.NUMBER, Style.CURRENCY, Style.PERCENT를 지정할 수 있다. 각각 지역 정보를 기준으로 숫자, 통화, 퍼센트로 표시된다. 그리고 @NumberFormat의 pattern 속성으로 숫자를 표현하는 패턴을 직접 지정할 수 있다. 참고로, 폼에 해당 패턴으로 입력이 들어오더라도 정해진 필드 타입으로 파싱이 자동으로 이루어진다.

public class Item {
    ...
    @NumberFormat("$###,##0.00")
    BigDecimal price;
    
    // setter, getter 추가
}

 

(2) @DateTimeFormat

- 문자열과 날짜/시간을 나타내는 타입의 오브젝트를 상호 변환해준다. JDK의 Date, Calendar, Long 타입과 Joda 라이브러리의 DateTime, LocalDate, LocalTime, LocalDateTime 타입의 필드에 적용할 수 있다.

- 스타일, ISO 형식, 그리고 직접 제공하는 패턴 중 하나를 지정할 수 있다.

- @DateTimeFormat의 style 속성으로 스타일을 지정할 수 있다. 단순함의 정도를 나타내는 S(short), M(medium), L(long), F(full)를 날짜와 시간에 대해 각각 한 글자씩 사용하면 되며, 디폴트는 "SS"다. 날짜 또는 시간은 스타일 문자 '-'를 지정하여 생략할 수 있다. 예를 들어, "S-"은 시간이 없는 날짜의 단순한 형식을 지정한다. 지역정보에 따라 스타일도 달라지는데, 아래와 같은 방법으로 스타일과 지역정보의 조합에 따른 포맷을 확인할 수 있다.

System.out.println(org.joda.time.format.DateTimeFormat.patternForStyle("SS", Locale.US); // M/d/yy h:mm a

- @DateTimeFormat의 iso 속성으로 DateTimeFormat.ISO 이늄 타입을 지정할 수 있다. 자세한 내용은 https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/format/annotation/DateTimeFormat.ISO.html 문서를 참고하자.

- @DateTimeFormat의 pattern 속성으로 커스텀 포맷을 지정할 수 있다.

@DateTimeFormat(pattern="yyy/MM/dd hh:mm:ss a")
Date date;

 

 

 

4. 바인딩 기술의 활용 전략

Converter

  • 자주 사용되는 사용자 정의 타입의 바인딩이 필요한 경우
  • 다른 bean을 활용한 변환 작업이 필요한 경우

 

ConditionalGenericConverter

  • 필드, 메소드 파라미터, 어노테이션 등의 메타 정보를 활용하여 세밀한 변환 조건을 지정해야 하는 경우 (복잡하고 구현이 까다롭기 때문에 따로 다루지는 않았다.)

 

AnnotationFormatterFactory와 Formatter

  • 필드에 부여하는 어노테이션 정보를 이용한 변환 작업이 필요한 경우
  • ConditionalGenericConverter를 사용하기 번거로운 경우

 

PropertyEditor

  • 특정 모델의 필드에만 적용되는 변환 작업이 필요한 경우 

※ 프로퍼티 에디터를 싱글톤 bean의 형태로 등록하고 싶거나, 프로퍼티 에디터 등록 작업을 여러 컨트롤러에서 반복적으로 진행해야 하는 경우에는 PropertyEditorRegistrar를 고려해 볼 만 하다.

 

- 타입 변환 기능에는 우선 순위가 있으니 참고하면 도움이 될 것이다.

커스텀 프로퍼티 에디터 > 컨버전 서비스의 컨버터 > 디폴트 프로퍼티 에디터

참고로, @InitBinder에서 직접 등록한 타입 변환 기능이 WebBindingInitializer로 일괄 적용한 타입 변환 기능보다 우선적으로 적용된다. Spring에서는 구체적인 설정이 우선적으로 적용되는 경향이 있다.

 

 

● 참고 자료 : 토비의 스프링 3.1 Vol.2

 

 

반응형

댓글