스프링 타입 컨버터(2) - Formatter

2022. 4. 11. 07:37Spring 기초

스프링 타입 컨버터(1) 포스팅에서 보았던
Converter 는 입력과 출력 타입에 제한이 없는, 범용 타입 변환 기능을 제공한다.

일반적인 웹 애플리케이션 환경을 생각해보자. 불린 타입을 숫자로 바꾸는 것 같은 범용 기능 보다는 개발자 입장에서는 문자를 다른 타입으로 변환하거나, 다른 타입을 문자로 변환하는 상황이 대부분이다.
예를들면,
화면에 숫자를 출력해야 하는데, Integer String 출력 시점에 숫자 1000 문자 "1,000" 이렇게 1000 단위에 쉼표를 넣어서 출력하거나, 또는 "1,000" 라는 문자를 1000 이라는 숫자로 변경해야 하는 경우.
날짜 객체를 문자인 "2021-01-01 10:50:11" 와 같이 출력하거나 또는 그 반대의 상황이 있겠다.

이렇게 객체를 특정한 포멧에 맞추어 문자로 출력하거나 또는 그 반대의 역할을 하는 것에 특화된 기능이 바로 포맷터(Formatter)이다. 포맷터는 컨버터의 특별한 버전으로 이해하면 된다.

Converter vs Formatter
Converter 는 범용(객체 객체)
Formatter 는 문자에 특화(객체 문자, 문자 객체) + 현지화(Locale)
=>Converter 의 특별한 버전


포맷터 - Formatter 만들기

포맷터( Formatter )는 객체를 문자로 변경하고, 문자를 객체로 변경하는 두 가지 기능을 모두 수행한다.
포맷터를 사용하려면, 컨버터때와 마찬가지로 인터페이스를 구현한 다음 등록해야 한다.

Formatter 인터페이스

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

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

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

 

포맷터를 이용해 숫자 1000 을 문자 "1,000" 으로 그러니까, 1000 단위로 쉼표가 들어가는 포맷을 적용해보자. 그리고 그 반대도 처리해주는 포맷터를 만들어보자.

@Slf4j
public class MyNumberFormatter implements Formatter<Number> {

 @Override
 public Number parse(String text, Locale locale) throws ParseException {
     log.info("text={}, locale={}", text, locale);
     NumberFormat format = NumberFormat.getInstance(locale);
 
 	 return format.parse(text);
 }
 
 @Override
 public String print(Number object, Locale locale) {
     log.info("object={}, locale={}", object, locale);
	 return NumberFormat.getInstance(locale).format(object);
 }
}

"1,000" 처럼 숫자 중간의 쉼표를 적용하려면 자바가 기본으로 제공하는 NumberFormat 객체를 사용하면 된다.
이 객체는 Locale 정보를 활용해서 나라별로 다른 숫자 포맷을 만들어준다.

parse() 를 사용해서 문자("1,000")를 숫자(1000L)로 변환한다. 참고로 Number 타입은 Integer , Long 과 같은 숫자 타입의 부모 클래스이다.
print() 를 사용해서 숫자를 문자로 변환한다.

다음은 테스트 코드이다.

class MyNumberFormatterTest {
 MyNumberFormatter formatter = new MyNumberFormatter();
 
 @Test
 void parse() throws ParseException {
     Number result = formatter.parse("1,000", Locale.KOREA);
     assertThat(result).isEqualTo(1000L); //Long 타입 주의
 }
 
 @Test
 void print() {
     String result = formatter.print(1000, Locale.KOREA);
     assertThat(result).isEqualTo("1,000");
 }
}
실행 결과 로그
MyNumberFormatter - text=1,000, locale=ko_KR
MyNumberFormatter - object=1000, locale=ko_KR

 

포맷터를 만들었으면, 등록하는 차례가 남았다. 그런데 컨버전 서비스에는 컨버터만 등록이 가능하다.  그런데 생각해보면 포맷터는 객체 -> 문자, 문자 -> 객체로 변환하는 특별한 컨버터일 뿐이다.
포맷터를 지원하는 컨버전 서비스를 사용하면 컨버전 서비스에 포맷터를 추가할 수 있다. 내부에서 어댑터 패턴을 사용해서 Formatter 가 Converter 처럼 동작하도록 지원한다.

FormattingConversionService 는 포맷터를 지원하는 컨버전 서비스이다. DefaultFormattingConversionService 는 FormattingConversionService 에 기본적인 통화, 숫자 관련 몇가지 기본 포맷터를 추가해서 제공한다.

따라서 DefaultFormattingConversionService를 이용해 다음과 같이 사용 가능하다.

@Test
 void formattingConversionService() {
 
 DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();

 //컨버터 등록
 conversionService.addConverter(new StringToIpPortConverter());
 conversionService.addConverter(new IpPortToStringConverter());
 
 //포맷터 등록
 conversionService.addFormatter(new MyNumberFormatter());
 
 //컨버터 사용
 IpPort ipPort = conversionService.convert("127.0.0.1:8080", IpPort.class);
 assertThat(ipPort).isEqualTo(new IpPort("127.0.0.1", 8080));
 
 //포맷터 사용
 assertThat(conversionService.convert(1000, String.class)).isEqualTo("1,000");
 assertThat(conversionService.convert("1,000", Long.class)).isEqualTo(1000L);
 }
}

포맷터를 웹 어플리케이션에 적용할때는

@Override
 public void addFormatters(FormatterRegistry registry) {
     //컨버터(1)에서 학습한 컨버터는 주석처리했다. 
     //NumberFormatter와 기능은 겹치면서 적용 우선순위가 더 높기때문이다.
     //registry.addConverter(new StringToIntegerConverter());
     //registry.addConverter(new IntegerToStringConverter());
     
     registry.addConverter(new StringToIpPortConverter());
     registry.addConverter(new IpPortToStringConverter());
     
     //추가
     registry.addFormatter(new MyNumberFormatter());
 }

위와 같이 등록하면 사용 가능하다.


스프링이 제공하는 기본 포맷터

포맷터는 기본 형식이 지정되어 있기 때문에, 객체의 각 필드마다 다른 형식으로 포맷을 지정하기는 어렵다.
스프링은 이런 문제를 해결하기 위해 애노테이션 기반으로 원하는 형식을 지정해서 사용할 수 있는 매우 유용한 포맷터 두 가지를 기본으로 제공한다.

  • @NumberFormat : 숫자 관련 형식 지정 포맷터 사용, NumberFormatAnnotationFormatterFactory
  • @DateTimeFormat : 날짜 관련 형식 지정 포맷터 사용, Jsr310DateTimeFormatAnnotationFormatterFactory
@Data
 static class Form {
     @NumberFormat(pattern = "###,###")
     private Integer number;

     @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private LocalDateTime localDateTime;
}

 


  • 컨버터, 포맷터 둘은 등록 방법은 다르지만, 사용할 때는 컨버전 서비스를 통해서 일관성 있게 사용할 수 있다.
  • 컨버전 서비스는 @RequestParam , @ModelAttribute , @PathVariable , 뷰 템플릿 등에서 사용할 수 있다