@RequestParam
같은 어노테이션을 사용할 때이다.
?a=100&b=dooboo
→Long a = 100L;
String b = "dooboo";
타입 컨버터
를 사용해야한다.//org.springframework.core.convert.conrvert.Converter
@FunctionalInterface
public interface Converter<S, T> {
@Nullable
T convert(S source);
// S의 타입을 입력받아 T의 타입을 반환한다.
//...
}
Converter
를 구현해야한다.public class StringToLongConverter implement Converter<String, Long> {
@Override
public Long convert(String source) {
return Long.valueOf(source);
}
}
String
→ 기본형 타입
변환 뿐 아니라, 특정 타입에서 타입의 변환을 정의 할 수 있다.Converter
로 타입 변환을 하기 위해서는 단순하게 new StringToIntegerConverter().convert(data);
를 매번 호출해서 변환을 해줘야하는데, 이는 매우 번거롭다.컨버전 서비스
가 존재한다.T convert(Object source)
를 이용한 사용package org.springframework.core.convert;
import org.springframework.lang.Nullable;
public interface ConversionService {
boolean canConvert(@Nullable Class<?> sourceType, Class<?> targetType);
boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType);
@Nullable
<T> T convert(@Nullable Object source, Class<T> targetType);
@Nullable
Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType);
}
void addConverter(Converter<?, ?> converter)
를 이용한 등록package org.springframework.core.convert.converter;
public interface ConverterRegistry {
void addConverter(Converter<?, ?> converter);
<S, T> void addConverter(Class<S> sourceType, Class<T> targetType, Converter<? super S, ? extends T> converter);
void addConverter(GenericConverter converter);
void addConverterFactory(ConverterFactory<?, ?> factory);
void removeConvertible(Class<?> sourceType, Class<?> targetType);
}
package org.springframework.core.convert.support;
// ConversionService와 ConverterRegistry를 구현
public class DefaultConversionService extends GenericConversionService {
public static void addDefaultConverters(ConverterRegistry converterRegistry) {
// 기본 컨버터 등록 ...
}
public static void addCollectionConverters(ConverterRegistry converterRegistry) {
// 컬렉션 관련 컨버터 등록 ...
}
private static void addScalarConverters(ConverterRegistry converterRegistry) {
// 기본 타입 컨버터 등록 ...
}
}
GenericConversionService
를 상속ConverterRegistry
의 등록하는 부분과 ConversionService
의 convert를 호출하여 변환하는 부분을 재정의 한다.package org.springframework.core.convert.support;
public class GenericConversionService implements ConfigurableConversionService {
@Override
public <S, T> void addConverter(Class<S> sourceType, Class<T> targetType, Converter<? super S, ? extends T> converter) {
addConverter(new ConverterAdapter(
converter, ResolvableType.forClass(sourceType), ResolvableType.forClass(targetType)));
}
@Override
public void addConverter(GenericConverter converter) {
this.converters.add(converter);
invalidateCache();
}
@Override
public void addConverterFactory(ConverterFactory<?, ?> factory) {
ResolvableType[] typeInfo = getRequiredTypeInfo(factory.getClass(), ConverterFactory.class);
if (typeInfo == null && factory instanceof DecoratingProxy) {
typeInfo = getRequiredTypeInfo(((DecoratingProxy) factory).getDecoratedClass(), ConverterFactory.class);
}
if (typeInfo == null) {
throw new IllegalArgumentException("Unable to determine source type <S> and target type <T> for your " +
"ConverterFactory [" + factory.getClass().getName() + "]; does the class parameterize those types?");
}
addConverter(new ConverterFactoryAdapter(factory,
new ConvertiblePair(typeInfo[0].toClass(), typeInfo[1].toClass())));
}
@Override
@SuppressWarnings("unchecked")
@Nullable
public <T> T convert(@Nullable Object source, Class<T> targetType) {
Assert.notNull(targetType, "Target type to convert to cannot be null");
return (T) convert(source, TypeDescriptor.forObject(source), TypeDescriptor.valueOf(targetType));
}
@Override
@Nullable
public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
Assert.notNull(targetType, "Target type to convert to cannot be null");
if (sourceType == null) {
Assert.isTrue(source == null, "Source must be [null] if source type == [null]");
return handleResult(null, targetType, convertNullSource(null, targetType));
}
if (source != null && !sourceType.getObjectType().isInstance(source)) {
throw new IllegalArgumentException("Source to convert from must be an instance of [" +
sourceType + "]; instead it was a [" + source.getClass().getName() + "]");
}
GenericConverter converter = getConverter(sourceType, targetType);
if (converter != null) {
Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType);
return handleResult(sourceType, targetType, result);
}
return handleConverterNotFound(source, sourceType, targetType);
}
@Nullable
public Object convert(@Nullable Object source, TypeDescriptor targetType) {
return convert(source, TypeDescriptor.forObject(source), targetType);
}
// ...
}
Converter<S, T>
등록하기 위해서는 WebMvcConfigurer
를 구현한 @Configuration
클래스에 addFormatters(FormatterRegistry registry)
를 재정의 하여 FormatterRegistry
에 addConverter(Converter converter)
를 통하여 컨버터를 등록 해줘야 한다.@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToLongConverter());
// ...
}
}
FormatterRegistry
는 CoverterRegistry
를 상속하였다.ConversionService
에 여기서 추가한 Converter
들이 추가된다.@Slf4j
@Controller
public class testController {
@GetMapping("/test")
public String test (@RequestParam Long a) {
log.info("a:{}", a);
}
}
/test?a=100 (요청 URL)
↓
a:100 (로그)
위와 같이 @RequestParam
으로 a
라는 파라미터를 받을 때 파라미터는 모두 String
으로 넘어오고 현재 이 매개변수의 타입은Long
이므로 String에서 Long으로 변환하는 컨버터를 찾아서 변환한다.
이는 @ModelAttribute
로 받을 때도 마찬가지이다.
@Slf4j
@Controller
public class testController {
@GetMapping("/test")
public String test (@ModelAttribute Dog dog) {
log.info(dog);
}
@Data
class Dog {
private String name;
private int age;
}
}
/test?name=두부&age=5 (요청 URL)
↓
Dog(name=두부, age=5) (로그)
${{...}}
를 통하여 컨버전 서비스를 통하여 String
으로 변환한다.${{}}
안에 넣게 되면 그것의 그 객체를 String으로 변환하는 컨버터를 찾아서 변환한다.변수 표현식 : ${dog} → test.Dog@23cd029
(이런 인스턴스에 대한 정보)컨버전 서비스 : ${{dog}} → 이름 : 두부, 나이 : 5살
(이런식으로 Dog를 String으로 변환하는 컨버터가 있는지 찾아서 변환한다)
th:object=${dto}
의 th:field="*{dog}"
컨버전 서비스를 적용할 수 있다.public class Dto {
private Dog dog;
}
<form th:object="${dto}">
<input type="text" th:field="*{dog}">
<input type="submit">
</form>
th:field="*{필드}"
로 컨버전 서비스를 적용하여 Dog에서 String으로 변환하는 컨버터를 찾아서 변환한다.Converter
는 객체를 다른 객체로 변환하고자 할 때 사용된다.Formatter
는 객체를 형식을 가진 문자
로 변환하거나, 형식을 가진 문자
를 객체로 변환할 때 사용하고, 현지화(Locale)
까지 적용하여 통화, 날짜와 같은 정보가 사용된다.Printer<T>
와 Parser<T>
를 상속받는 인터페이스package org.springframework.format;
public interface Formatter<T> extends Printer<T>, Parser<T> {
}
@FunctionalInterface
public interface Printer<T> {
String print(T object, Locale locale);
}
@FunctionalInterface
public interface Parser<T> {
T parse(String text, Locale locale) throws ParseException;
}
public class MyNumberFormatter implements Formatter<Number> {
@Override
public Number parse(String text, Locale locale) throws ParseException {
//"1,000" -> 1000
return NumberFormat.getInstance(locale).parse(text);
}
@Override
public String print(Number object, Locale locale) {
//1000 -> "1,000"
return NumberFormat.getInstance(locale).format(object);
}
}
java.lang.Number는 숫자형 자료형의 부모 클래스이다.
FormattingConversionService
를 상속하는 컨버전 서비스다GenericConversionService
를 상속하고FormatterRegistry
를 구현한 클래스이다,ConversionService
의 자식이므로 convert()
를 사용하여 포매터를 호출할 수 있다.@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
// 컨버터 적용
registry.addConverter(new StringToLongConverter());
// ...
// 포매터 적용
registry.addFormatter(new MyNumberFormatter());
}
}
@NumberFormat
숫자 형식을 변환하는 포매터@DateTimeFormat
날짜 시간 형식을 변환하는 포매터 public final class NumberFormatAnnotationFormatterFactory implements AnnotationFormatterFactory<NumberFormat> {
public Set<Class<?>> getFieldTypes() {
return new HashSet<Class<?>>(asList(new Class<?>[] {
Short.class, Integer.class, Long.class, Float.class,
Double.class, BigDecimal.class, BigInteger.class }));
}
public Printer<Number> getPrinter(NumberFormat annotation, Class<?> fieldType) {
return configureFormatterFrom(annotation, fieldType);
}
public Parser<Number> getParser(NumberFormat annotation, Class<?> fieldType) {
return configureFormatterFrom(annotation, fieldType);
}
private Formatter<Number> configureFormatterFrom(NumberFormat annotation, Class<?> fieldType) {
if (!annotation.pattern().isEmpty()) {
return new NumberStyleFormatter(annotation.pattern());
} else {
Style style = annotation.style();
if (style == Style.PERCENT) {
return new PercentStyleFormatter();
} else if (style == Style.CURRENCY) {
return new CurrencyStyleFormatter();
} else {
return new NumberStyleFormatter();
}
}
}
}
@Data
public class Dto {
@NumberFormat(pattern= "###,###")
private Long money
}
/test?money=100000
↓
Dto(money = 100,000)
AnnotationFormatterFactory
의 구현체를 통하여 적절한 포매터에 해당 필드에 바인딩 데이터를 변환한다.객체
↔ 스트링
로 변환하는 것이기 때문에, 컨버터와 포매터는 우선순위에 따라 적용된다.컨버터가 우선순위가 높다
고 생각할 수 있다.