[Spring] 타입 컨버터와 포매터

dooboocookie·2022년 12월 26일
0
post-thumbnail

스프링 타입 컨버터

  • Spring mvc로 웹 프로그래밍을 하다보면 마법같은 일이 많이 일어난다.
  • 그 중 제일 처음 느꼈던 마법같은 일은 @RequestParam같은 어노테이션을 사용할 때이다.
  • HTTP의 파라미터들은 String으로 넘어오는데, 그를 변수나 필드에 바인딩할 때 타입을 맞춰준다는 것이다.

?a=100&b=doobooLong 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의 구현체들이 존재한다.

컨버전 서비스

  • 위에서 구현한 Converter로 타입 변환을 하기 위해서는 단순하게 new StringToIntegerConverter().convert(data);를 매번 호출해서 변환을 해줘야하는데, 이는 매우 번거롭다.
  • 이를 해결하기 위해 컨버터 중 맞는 컨버터를 상황에 맞게 제공하기 위한 컨버전 서비스가 존재한다.

ConversionService

  • 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);

}

ConversionResitry

  • 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);

}

DefaultConversionService

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

	// ...

}

스프링의 컨버터 등록 및 적용 - WebMvcConfigurer

  • 스프링에 전반적으로 적용하기 위한 Converter<S, T> 등록하기 위해서는 WebMvcConfigurer를 구현한 @Configuration클래스에 addFormatters(FormatterRegistry registry)를 재정의 하여 FormatterRegistryaddConverter(Converter converter)를 통하여 컨버터를 등록 해줘야 한다.
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToLongConverter());
        // ...
    }
}
  • FormatterRegistryCoverterRegistry를 상속하였다.
  • 이렇게 되면 스프링 내부에서 사용되는 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) (로그)

Thymeleaf에서 컨버전 서비스 적용

  • ${{...}}를 통하여 컨버전 서비스를 통하여 String으로 변환한다.
    • 기본적으로 Integer같은 숫자 자료형은 타임리프가 자동으로 변환하기 때문에, 컨버전 서비스를 적용하나 안하나 결과는 같다.
    • 객체를 ${{}}안에 넣게 되면 그것의 그 객체를 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)까지 적용하여 통화, 날짜와 같은 정보가 사용된다.

Formatter 인터페이스

  • Printer<T>Parser<T>를 상속받는 인터페이스
package org.springframework.format;

public interface Formatter<T> extends Printer<T>, Parser<T> {

}
  • Printer는 어떤 객체를 String으로 변환하는 역할을 한다.
@FunctionalInterface
public interface Printer<T> {

	String print(T object, Locale locale);

}
  • Parser는 String을 어떤 객체로 변환하는 역할을 한다.
@FunctionalInterface
public interface Parser<T> {

	T parse(String text, Locale locale) throws ParseException;

}

Formatter 구현

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는 숫자형 자료형의 부모 클래스이다.

  • 이런식으로 Locale 정보와 형식이 있느는 String을 변환하는데 사용한다.

DefaultFormattingConversionService

  • FormattingConversionService를 상속하는 컨버전 서비스다
    • 위에서 다뤘던 GenericConversionService를 상속하고
    • FormatterRegistry를 구현한 클래스이다,
  • 기본적으로 Converter와 내용은 크게 다를 것이 없다.
  • ConversionService의 자식이므로 convert()를 사용하여 포매터를 호출할 수 있다.

Formatter 등록 및 적용

@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의 구현체를 통하여 적절한 포매터에 해당 필드에 바인딩 데이터를 변환한다.

스프링 MVC에서 우선순위

  • 포매터는 특별한 형식을 가진 스트링으로 변환해주는 컨버터에 가깝다.
  • 포매터의 객체스트링로 변환하는 것이기 때문에, 컨버터와 포매터는 우선순위에 따라 적용된다.
  • 스프링에선 자세한 것일 수록 우선순위가 높다.
    • 포매터는 스트링으로 변환되거나 스트링을 변환하는 설정이므로 양쪽의 객체를 설정하는 컨버터가 우선순위가 높다고 생각할 수 있다.
  1. 등록한 컨버터
  2. 등록되어 있는 컨버터
  3. 등록한 포매터
  4. 등록되어 있는 포매터
profile
1일 1산책 1커밋

0개의 댓글