본문 바로가기

Spring

[Spring] Formatter

 


 

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