Formatter
정의
객체를 문자열로 변환하거나 문자열을 객체로 변환하는 과정에서 특정한 포맷에 맞춰서 출력하는 시스템입니다.
- 단순히 타입을 변환시키는 `Converter`보다 조금 더 세부적인 기능이라고 생각하면됩니다.
- `Converter`는 데이터끼리 변환하는 변환기
- `Formatter`는 사람이 보기 좋게/입력하기 쉽게 포장해주는 포장기
내부 구조
public interface Formatter<T> extends Printer<T>, Parser<T> {
}
- `Printer`, `Parser` 상속받고 있습니다.
`Printer`
@FunctionalInterface
public interface Printer<T> {
String print(T object, Locale locale);
}
- Object를 String으로 변환하는 기능입니다.
`Parser`
@FunctionalInterface
public interface Parser<T> {
T parse(String text, Locale locale) throws ParseException;
}
- String을 Object로 변환하는 기능입니다.
Locale
정의
지역 및 언어 정보를 나타내는 객체입니다.
코드
- 언어코드 `en`, `ko`
- 국가코드 `US`, `KR`
국제화
- Locale 정보에 따라서 한글을 보여줄지 영문을 보여줄지 선택할 수 있습니다.
✨ 특정 지역 및 언어에 대한 정보를 제공하여 국제화 및 지역화 기능을 지원합니다.
코드 예시
숫자(`10000`)를 금액 형태(`10,000`)로 변환하는 `Formatter`입니다.
@Slf4j
public class PriceFormatter implements Formatter<Number> {
@Override
public Number parse(String text, Locale locale) throws ParseException {
log.info("text = {}, locale={}", text, locale);
// 변환 로직
// NumberFormat이 제공하는 기능
NumberFormat format = NumberFormat.getInstance(locale);
// "10,000" -> 10000L
return format.parse(text);
}
@Override
public String print(Number object, Locale locale) {
log.info("object = {}, locale = {}", object, locale);
// 10000L -> "10,000"
return NumberFormat.getInstance(locale).format(object);
}
}
- Custom Formatter를 만들기 위해 Formatter<>를 구현했습니다.
- Formatter<Number>에서 Number를 사용하는 이유는, `Integer`, `Double`, `Long` 등 모든 숫자형 래퍼 클래스의 상위 클래스이기 때문입니다. 따라서 다양한 숫자 타입을 처리할 수 있습니다.
class PriceFormatterTest {
PriceFormatter formatter = new PriceFormatter();
@Test
void parse() throws ParseException {
// given, when
Number result = formatter.parse("1,000", Locale.KOREA);
// then
// parse 결과는 Long
Assertions.assertThat(result).isEqualTo(1000L);
}
@Test
void print() {
// given, when
String result = formatter.print(1000, Locale.KOREA);
// then
Assertions.assertThat(result).isEqualTo("1,000");
}
}
- custom formatter를 인스턴스화 한 후 사용합니다.
- parse() : 테스트 통과
- print() : 테스트 통과
✨ 만약, Converter도 사용해야하고 Formatter도 함께 사용해야한다면 이미 만들어진 `FormattingConversionService` 구현체를 사용하면 됩니다.
FormattingConversionService
정의
`ConversionService`와 `Formatter`를 결합한 구현체로 타입 변환(Convert)과 포맷팅(Formatting)이 필요한 모든 작업을 한 곳에서 수행할 수 있도록 설계되어 있어, 다양한 타입의 변환과 포맷팅을 쉽게 적용할 수 있는 구현체입니다.
내부 구조
public class FormattingConversionService extends GenericConversionService implements FormatterRegistry, EmbeddedValueResolverAware {
@Nullable
private StringValueResolver embeddedValueResolver;
private final Map<AnnotationConverterKey, GenericConverter> cachedPrinters = new ConcurrentHashMap(64);
private final Map<AnnotationConverterKey, GenericConverter> cachedParsers = new ConcurrentHashMap(64);
public FormattingConversionService() {
}
public void setEmbeddedValueResolver(StringValueResolver resolver) {
this.embeddedValueResolver = resolver;
}
public void addPrinter(Printer<?> printer) {
Class<?> fieldType = getFieldType(printer, Printer.class);
this.addConverter(new PrinterConverter(fieldType, printer, this));
}
public void addParser(Parser<?> parser) {
Class<?> fieldType = getFieldType(parser, Parser.class);
this.addConverter(new ParserConverter(fieldType, parser, this));
}
public void addFormatter(Formatter<?> formatter) {
this.addFormatterForFieldType(getFieldType(formatter), formatter);
}
...
}
다이어 그램
- 어댑터 패턴을 사용하여 `Formatter`가 `Converter`처럼 동작하도록 만들어줍니다.
✨ `FormattingConversionService`가 `Formatter`를 내부적으로 `Converter`처럼 감싸서 사용합니다. 이를 어댑터 패턴이 사용되었다고 합니다.
- Formatter를 Converter 사용하 듯이 사용할 수 있게 됩니다. (자동 변환 등)
예시 코드
import org.springframework.format.Formatter;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
public class LocalDateFormatter implements Formatter<LocalDate> {
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
@Override
public LocalDate parse(String text, Locale locale) {
return LocalDate.parse(text, formatter);
}
@Override
public String print(LocalDate object, Locale locale) {
return formatter.format(object);
}
}
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addFormatter(new LocalDateFormatter());
}
}
@GetMapping("/date")
public String handle(@RequestParam LocalDate date) {
// "2025-06-09" 같은 문자열을 자동으로 LocalDate로 변환!
return "Parsed date: " + date;
}
- WebConfig에 등록되어 있어야지, 자동으로 변환할 수 있습니다.
1. Formatter만 사용하는 경우
- 직접 parse()와 print()를 수동으로 호출해야 합니다.
- 자동 변환 기능이 없습니다.
LocalDateFormatter formatter = new LocalDateFormatter();
LocalDate date = formatter.parse("2025-06-09", Locale.getDefault());
String str = formatter.print(date, Locale.getDefault());
즉, 직접 파싱하거나 출력해야 해야 합니다.
이건 Formatter가 그저 변환 도구일 뿐이고, 자동 변환 환경과는 별개입니다.
2. FormattingConversionService를 사용하는 경우
- Formatter를 내부적으로 Converter처럼 자동으로 감싸서 등록합니다.
- 이제 ConversionService의 convert() 메서드로 자동 변환이 됩니다.
- 마치 Converter<String, LocalDate> 또는 Converter<LocalDate, String>을 등록한 것처럼 동작합니다.
LocalDate date = conversionService.convert("2025-06-09", LocalDate.class);
String str = conversionService.convert(date, String.class);
Spring Boot의 기ㄴ
Spring Boot는 기본적으로 `WebConversionService`를 사용합니다.
`WebConversionService`는 `DefaultFormattingConversionService`를 상속받습니다.
`DefaultFormattingConversionService`는 `FormattingConversionService`를 상속 받고, 통화, 숫자 관련 Formatter가 추가되어있습니다.
- `FormattingConversionService`의 기능을 그대로 사용할 수 있습니다.
참조 문헌
https://docs.spring.io/spring-framework/reference/core/validation/format.html
Spring Field Formatting :: Spring Framework
As discussed in the previous section, core.convert is a general-purpose type conversion system. It provides a unified ConversionService API as well as a strongly typed Converter SPI for implementing conversion logic from one type to another. A Spring conta
docs.spring.io