문자를 숫자로 변환하거나, 반대로 숫자를 문자로 변환해야 하는 것처럼 애플리케이션을 개발하다 보면 타입을 변환해야 하는 경우가 상당히 많다.

스프링이 중간에 타입 변환기를 사용해서 타입을 String → Integer 로 변환해주었기 때문에 개발자는 편리하게 해당 타입을 바로 받을 수 있다

컨버터 인터페이스

package org.springframework.core.convert.converter;

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

컨버전 서비스

개별 컨버터를 모아두고 그것들을 묶어서 편리하게 사용할 수 있는 기능

public class ConversionServiceTest {

    @Test
    void conversionService(){
        //등록
        DefaultConversionService conversionService = new DefaultConversionService();
        conversionService.addConverter(new StringToIntegerConverter());
        conversionService.addConverter(new IntegerToStringConverter());
        conversionService.addConverter(new StringToIpPortConverter());
        conversionService.addConverter(new IpPortToStringConverter());

        //사용
        assertThat(conversionService.convert("10",Integer.class)).isEqualTo(10);
        assertThat(conversionService.convert(10, String.class)).isEqualTo("10");

        IpPort ipPort = conversionService.convert("127.0.0.1:8080", IpPort.class);
        assertThat(ipPort).isEqualTo(new IpPort("127.0.0.1", 8080));

        String ipPortString = conversionService.convert(new IpPort("127.0.0.1",8080), String.class);
        assertThat(ipPortString).isEqualTo("127.0.0.1:8080");
    }
}

등록과 사용 분리

등록할 때는 StringToIntegerConverter 같은 타입 컨버터를 명확하게 알아야 한다. 반면에 컨버터를 사용하는 입장에서는 타입 컨버터를 전혀 몰라도 된다.

컨버전 서비스를 등록하는 부분과 사용하는 부분을 분리하고 의존관계 주입을 사용해야 한다.

인터페이스 분리 원칙 - ISP(Interface Segregation Principle)

인터페이스 분리 원칙은 클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다.

DefaultConversionService 는 다음 두 인터페이스를 구현했다.

인터페이스를 분리하면 컨버터를 사용하는 클라이언트와 컨버터를 등록하고 관리하는 클라이언트의 관심사를 명확하게 분리할 수 있다.

특히 컨버터를 사용하는 클라이언트는 ConversionService 만 의존하면 되므로, 컨버터를 어떻게 등록하고 관리하는지는 전혀 몰라도 된다.

결과적으로 컨버터를 사용하는 클라이언트는 꼭 필요한 메서드만 알게된다. 이렇게 인터페이스를 분리하는 것을 ISP 라 한다.

스프링에 Converter 적용

컨버터 등록

@Configuration
public class WebConfig implements WebMvcConfigurer {
 @Override
 public void addFormatters(FormatterRegistry registry) {
	 registry.addConverter(new StringToIntegerConverter());
	 registry.addConverter(new IntegerToStringConverter());
	 registry.addConverter(new StringToIpPortConverter());
	 registry.addConverter(new IpPortToStringConverter());
 }
}

뷰 템플릿에 Converter 적용

<ul>
    <!-- 대괄호 2개 : 컨버터 사용 -->
    <li>${number}: <span th:text="${number}" ></span></li>
    <li>${{number}}: <span th:text="${{number}}" ></span></li>  <!-- IntergerToStringConverter 동작 -->
    <li>${ipPort}: <span th:text="${ipPort}" ></span></li>
    <li>${{ipPort}}: <span th:text="${{ipPort}}" ></span></li>  <!-- IpPortToStringConverter 동작 -->
</ul>
<form th:object="${form}" th:method="post">
    th:field <input type="text" th:field="*{ipPort}"><br/>
    th:value <input type="text" th:value="*{ipPort}">(보여주기 용도)<br/>
    <input type="submit"/>
</form>

th:field 에서 컨버전 서비스 기능 제공.

포매터 - Formatter

converter는 범용 (객체 → 객체)

Formatter는 문자에 특화 (객체 → 문자, 문자 → 객체) + 현지화(Locale)

포매터 인터페이스

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> {
}
@Slf4j
public class MyNumberFormatter implements Formatter<Number> {

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

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

String print(T object, Locale locale) : 객체를 문자로 변경한다. T parse(String text, Locale locale) : 문자를 객체로 변경한다.

스프링 제공 기본 포매터

애노테이션 기반으로 원하는 형식을 지정해서 사용할 수 있는 매우 유용한 포맷터 두 가지를 기본으로 제공한다.

@NumberFormat : 숫자 관련 형식 지정 포맷터 사용

@DateTimeFormat : 날짜 관련 형식 지정 포맷터 사용

@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;
    }
}

실행 결과

Untitled

Untitled