Formatter

  • 실무에서는 문자와 다른 타입 간의 변환이 자주 일어난다.

    • ex. 화면에 숫자나 날짜를 문자 형태로 출력해야 할 때

    • 특히 날짜와 숫자 표현은 Locale 즉, 나라마다 달라질 수도 있다.

Formatter는 이렇게 특정한 포맷과 문자 간의 변환을 담당하는 컨버터의 특별한 버전이다.

Converter vs Formatter

  • Converter

    • 범용적인 기능

    • 객체와 객체의 변환

  • Formatter

    • 문자에 특화된 기능

    • Locale(현지화) 기능

    • Converter의 특별한 버전

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> {
}
  • print()

    • 객체를 문자로 변환한다.

  • parse()

    • 문자를 객체로 변환한다.

@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);
    }
}
  • Number

    • Integer, Long 등 숫자의 부모

  • NumberFormat

    • 숫자에 쉼표를 적용하거나 Locale 정보를 활용해 나라 별로 다른 숫자 포맷을 만들어준다.

class MyNumberFormatterTest {

    MyNumberFormatter formatter = new MyNumberFormatter();

    @Test
    void parse() throws ParseException {
        Number result = formatter.parse("1,000", Locale.KOREA);
        assertThat(result).isEqualTo(1000L);
    }

    @Test
    void print() {
        String result = formatter.print(1000, Locale.KOREA);
        assertThat(result).isEqualTo("1,000");
    }
}

테스트 해보면 문자를 숫자로, 숫자를 한국 형식에 맞는 문자로 변환한다.

Formatter

Foramtter를 지원하는 ConversionService

  • ConversionService에는 Converter만 등록할 수 있고 Formatter는 불가하다.

  • Foramtter도 결국 Converter의 한 종류이기 때문에 Formatter를 지원하는 ConversionService를 통해 추가할 수 있다.

FormattingConversionService

public class FormattingConversionServiceTest {

    @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);
    }
}
  • Formatter를 지원하는 컨버전 서비스

  • DefaultFormattingConversionService

    • FormattingConversionService에 통화, 숫자 등 추가적인 기능을 제공한다.

상속 관계

  • FormattingConversionService는 ConversionService 기능을 상속받는다.

    • 따라서 컨버터와 포매터 모두 등록할 수 있다.

  • 사용할 때는 ConversionService.convert()를 사용하면 된다.

  • 스프링 부트는 DefaultFormattingConversionService를 상속 받은 WebConversionService를 사용한다.

Formatter 적용

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        /*
        포매터보다 컨버터가 우선 순위가 높기 때문에
        포매터와 비슷한 기능을 수행하는 컨버터를 주석 처리 한다.
        registry.addConverter(new IntegerToStringConverter());
        registry.addConverter(new StringToIntegerConverter());
        */
        registry.addConverter(new IpPortToStringConverter());
        registry.addConverter(new StringToIpPortConverter());

        // 포매터를 추가한다.
        registry.addFormatter(new MyNumberFormatter());
    }
}
# before
${number}: 10000
${{number}}: 10000
${ipPort}: hello.typeconverter.type.IpPort@59cb0946
${{ipPort}}: 127.0.0.1:8080
# after
${number}: 10000
${{number}}: 10,000
${ipPort}: hello.typeconverter.type.IpPort@59cb0946
${{ipPort}}: 127.0.0.1:8080
  • WebConfig에 포매터를 추가한다.

  • 숫자에 쉼표를 붙이는 포매터가 적용되었다.

스프링이 제공하는 기본 Formatter

  • 스프링은 자바의 기본 타입에 대해 Formatter를 기본으로 제공한다.

  • Formatter는 기본 형식이 지정되어 있어서 필드마다 다른 형식을 지정하기 힘들다.

이 문제를 해결하기 위해 스프링은 애너테이션으로 형식을 지정할 수 있게 지원한다.

  • @NumberFormat

    • 숫자 관련 형식 지정

    • NumberFormatAnnotationFormatterFactory

  • @DateTimeFormat

    • 날짜 관련 형식 지정

    • Jsr310DateTimeFormatAnnotationFormatterFactory

@Controller
public class FormatterController {

    @GetMapping("/formatter/edit")
    public String formatterForm(Model model) {
        Form form = new Form();
        
        form.setNumber(10000);
        form.setLocalDateTime(LocalDateTime.now());
        
        model.addAttribute("form", form);
        return "formatter-form";
    }

    @PostMapping("/formatter/edit")
    public String formatterEdit(@ModelAttribute Form form) {
        return "formatter-view";
    }

    @Data
    static class Form {

        @NumberFormat(pattern = "###,###")
        private Integer number;

        @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
        private LocalDateTime localDateTime;
    }
}
  • 지정한 포맷대로 값이 준비되어 있다.

  • 결과를 보면 포매터를 적용 안 한 것과 한 것의 차이가 보인다.

스프링의 기본 Formatter

Last updated