@GetMapping("/hello-v2")
public String helloV2(@RequestParam Integer data) {
System.out.println("data = " + data);
return "ok";
이제는 익숙한 표현인 파라미터에 직접 매핑할 객체를 전달해서 http 요청에 담긴 쿼리 스트링과 객체의 필드를 연결하여 필드에 주입을 받고 컨트롤러로 넘어오는 과정에는 자동 타입 변환 기능이 적용된다. http에서 넘어오는 쿼리 파라미터는 String 형식이지만 스프링의 타입 컨버터에 의해 자동 형변환을 거쳐 필드에 들어온다.
@RequestParam뿐만 아니라 @ModelAttribute나 @PathVariable, 뷰 렌더링에서까지 지원한다.
package org.springframework.core.convert.converter;
public interface Converter<S, T> {
T convert(S source);
}
스프링은 위와 같은 컨버터 인터페이스를 제공한다. 이 컨버터 인터페이스를 구현해서 등록하면 자동 타입 변환 기능을 직접 구현할 수 있다.
위의 예시에서는 String -> Integer와 같은 흔한 형 변환을 수행했지만 직접 만든 타입을 적용하여 형 변환할 수 있다. 심지어는 그 절차에 개발자가 원하는 특별한 로직을 넣어도 상관없다.
@Slf4j
public class StringToIpPortConverter implements Converter<String, IpPort> {
@Override
public IpPort convert(String source) {
log.info("convert source={}", source);
//127.0.0.1:8080
String[] split = source.split(":");
String ip = split[0];
int port = Integer.parseInt(split[1]);
return new IpPort(ip, port);
}
}
타입 컨버터를 직접 찾아서 타입 변환에 사용하는 것은 매우 불편하다. 이들을 묶어 편리하게 사용할 수 있는 기능이 바로 ConversionService이다. 우리가 실제로 사용할 때 ConversionService는 드러나지 않는다. 개발자를 위한 너무 많은 생략이 존재한다. 그 과정을 모두 이해하는 것이 이번 챕터의 목적이다. 컨버전 서비스는 그 과정에 있어 가장 중요한 역할을 한다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new IpPortToStringConverter());
registry.addConverter(new StringToIpPortConverter());
// 추가
registry.addFormatter(new MyNumberFormatter());
}
}
내장 컨버터와 상관없이 우리는 위와 같이 addFormatters메서드를 구현하여 FormatterRegistry에 컨버터를 등록할 수 있다. 이 레지스트리를 활용하여 Formatter를 검증하고 Formatter의 기능을 적용하는 일은 ConversionService에서 일어난다.
public interface ConversionService {
boolean canConvert(@Nullable Class<?> sourceType, Class<?> targetType);
boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor
targetType);
<T> T convert(@Nullable Object source, Class<T> targetType);
Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType,
TypeDescriptor targetType);
}
ConversionService는 단순히 컨버팅이 가능한지 그리고 컨버팅 기능을 제공한다. FormatterRegistry에 등록된 컨버터를 찾을 것이고 그 컨버터로 컨버팅을 진행시켜줄 것이다.
이렇게 컨버터를 등록하는 곳과 컨버터를 실행하는 곳을 분리하여 실행은 자동화 시켜놓고 우리는 등록에만 관여하면 컨버터가 우리가 사용하는 애노테이션 방식에서 잘 동작할 수 있다.
간단하다 타임리프에서 컨버전 서비스 적용을 위해서는 ${{...}}를 사용하면 된다.(변수 표현식에 {}추가한 모양)
${ipPort}: hello.typeconverter.type.IpPort@59cb0946
${{ipPort}}: 127.0.0.1:8080
단순히 객체를 랜더링하면 toString()이 작동하지만, ${{...}}를 사용하면 IpPortToString이 적용된다. 타임리프는 제네릭 표현을 참고하여 IpPort에서 String으로 변할 수 있는 컨버터를 찾게된다.
<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는 컨버전 서비스를 자동으로 적용해준다.
포메터는 컨버터의 한 종류로, String to X, X to String 두 가지 기능을 구현한다. Http 통신으로 들어오는 데이터는 대부분 String 형식이고 우리가 html을 랜더링 하기 위해 사용되는 컨버터또한 String타입으로의 전환 혹은 String타입으로 부터의 전환이므로 그러하다.
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(to String)와 parse(from String) 메서드를 구현할 수 있다. WebConfig에 등록시에 컨버터와 다르지 않게 등록할 수 있으며 registry.addFormatter로 등록가능하다.
스프링은 자바에서 기본으로 제공하는 수 많은 타입들에 대하여 그에 맞는 포멧터들을 기본으로 제공한다. 객체의 각 필드마다 다른 형식으로 포멧을 지정하기 위해 애노테이션 기반으로 원하는 형식을 지정하여 사용할 수 있다.
static class Form {
@NumberFormat(pattern = "###,###")
private Integer number;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime localDateTime;
}
위와 같이 형식을 지정할 경우 number가 뷰에서 랜더링 되기 위해 String으로 변환될 시에 1000과 같은 숫자 표현이 단순히 형만 바뀌는 것이 아닌 1,000으로 랜더링된다.