[Spring] 파라미터 변환을 위한 Convert 생성 및 등록 방법
목차
Converter
정의
Spring에서 특정 타입을 다른 타입으로 변환할 때 사용하는 인터페이스로 타입 변환 로직을 캡슐화하여 코드의 재사용성을 높이고 다양한 곳에서 타입 변환이 일관되게 수행되도록 돕습니다.
Converter, Custom Converter가 필요한 이유
✨ `HttpServletRequest`를 통해 값을 전달받은 후, 컨트롤러 메서드에서 다음과 같이 값을 변환해주는 코드를 직접 작성해야합니다.
@Slf4j
@RestController
public class TypeConverterController {
@GetMapping("/param")
public void param(HttpServletRequest request) {
// 조회시 : String
String stringExample = request.getParameter("example");
// Integer로 Type 변환
Integer integerExample = Integer.valueOf(stringExample);
log.info("integerExample = {}", integerExample);
}
}
- 컨트롤러 메서드를 통해 Http 요청을 받습니다.
- HttpServletRequest 객체를 통해 Parameter 값을 받습니다.
- Key가 "example"인 Parameter의 Value 값을 가져옵니다.
- Parameter의 Value 값은 항상 String입니다.
- 문자열을 정수형으로 바꿔주기 위해 `Integer.valueOf()`를 사용하여Value의 Type을 변환합니다.
- 컨트롤러 메서드에서 Parameter로 값을 받기 위해, Params의 `Key=Value`로 값을 전달합니다.
- 값이 성공적으로 바뀌었습니다.
✨ 값을 변환해주는 코드를 매번 작성해야해 번거롭고, 컨트롤러의 역할 + 타입 변환 역할이 추가되어 SRP의 원칙에 위배가 됩니다.
내부 구조
@FunctionalInterface
public interface Converter<S, T> {
@Nullable
T convert(S source);
default <U> Converter<S, U> andThen(Converter<? super T, ? extends U> after) {
Assert.notNull(after, "'after' Converter must not be null");
return (s) -> {
T initialResult = (T)this.convert(s);
return initialResult != null ? after.convert(initialResult) : null;
};
}
}
- Spring이 제공하는 인터페이스입니다.
- 먼저, 이 인터페이스를 구현한 후, 두번 째 Converter로 등록하면 됩니다.
- Converter는 모든 타입(`T`)에 적용할 수 있습니다.
- `S` : 변환할 Source
- `T` : 변환할 Type
- 개발자가 새로운 Type을 만들어서 사용할 수 있도록 만듭니다.
- 변환하고자 하는 타입에 맞춰서 Converter를 구현하고 등록하면됩니다.
구현 예시
public class StringToPersonConverter implements Converter<String, Person> {
// source = "yongjun:25"
@Override
public Person convert(String source) {
// ':' 를 구분자로 나누어 배열로 만든다.
String[] parts = source.split(":");
// 첫번째 배열은 이름이다. -> yongjun
String name = parts[0];
// 두번째 배열은 나이이다. -> 25
int age = Integer.parseInt(parts[1]);
return new Person(name, age);
}
}
- Converter를 자동으로 사용하기 위해서는 구현 후, 등록해야 합니다.
등록하지 않고 `Custom Converter`를 직접 생성(인스턴스화)하여 사용한다면?
❌ 컨트롤러에서 직접 타입을 변환하는 방식과 큰 차이가 없게 됩니다.
Custom Converter를 Controller에서 직접 생성
@PostMapping
public void StringToPersonController(HttpServletRequest request) {
// param = "person=yongjun:25"
PersonToStringConverter converter = new PersonToStringConverter(); // 직접 생성
String key = request.getParameter("person");
converter.convert(key);
}
public class StringToPersonConverter implements Converter<String, Person> {
// key = "yongjun:25"
@Override
public Person convert(String key) {
// ':' 를 구분자로 나누어 배열로 만든다.
String[] parts = source.split(":");
// 첫번째 배열은 이름이다. -> yongjun
String name = parts[0];
// 두번째 배열은 나이이다. -> 25
int age = Integer.parseInt(parts[1]);
return new Person(name, age);
}
}
컨트롤러에서 타입 변환 직접 진행
@PostMapping
public void StringToPersonController(HttpServletRequest request) {
// param = "person=yongjun:25"
// ':' 를 구분자로 나누어 배열로 만든다.
String[] parts = source.split(":");
// 첫번째 배열은 이름이다. -> yongjun
String name = parts[0];
// 두번째 배열은 나이이다. -> 25
int age = Integer.parseInt(parts[1]);
return new Person(name, age);
}
✨따라서 Converter를 편리하게 등록하고 사용할 수 있도록 만들어주는 `DefaultConversionService`를 사용하면 좋습니다.
Spring 제공 Converter
- Spring은 `String`, `Integer`, `Enum` 등 자주 사용되는 타입에 대한 다양한 컨버터 구현체를 기본으로 제공하고 자동으로 사용할 수 있도록 등록되어 있습니다.
- 따라서 특수한 경우가 아니면, 새로 구현할 필요 없이 등록된 Converter를 사용하면 됩니다.
✨ Spring은 이미 자주 사용하는 Converter는 구현 후 등록해 놓은 상태입니다.
DefaultConversionService
정의
Spring의 표준 `ConversionService`로 기본 제공 `Converter`와 확장 가능성을 통해 다양한 타입 변환을 유연하게 처리할 수 있도록 지원합니다.
내부 구조
public class DefaultConversionService extends GenericConversionService {
@Nullable
private static volatile DefaultConversionService sharedInstance;
public DefaultConversionService() {
addDefaultConverters(this);
}
public static ConversionService getSharedInstance() {
DefaultConversionService cs = sharedInstance;
if (cs == null) {
synchronized(DefaultConversionService.class) {
cs = sharedInstance;
if (cs == null) {
cs = new DefaultConversionService();
sharedInstance = cs;
}
}
}
return cs;
}
public static void addDefaultConverters(ConverterRegistry converterRegistry) {
addScalarConverters(converterRegistry);
addCollectionConverters(converterRegistry);
converterRegistry.addConverter(new ByteBufferConverter((ConversionService)converterRegistry));
converterRegistry.addConverter(new StringToTimeZoneConverter());
converterRegistry.addConverter(new ZoneIdToTimeZoneConverter());
converterRegistry.addConverter(new ZonedDateTimeToCalendarConverter());
converterRegistry.addConverter(new ObjectToObjectConverter());
converterRegistry.addConverter(new IdToEntityConverter((ConversionService)converterRegistry));
converterRegistry.addConverter(new FallbackObjectToStringConverter());
converterRegistry.addConverter(new ObjectToOptionalConverter((ConversionService)converterRegistry));
}
public static void addCollectionConverters(ConverterRegistry converterRegistry) {
ConversionService conversionService = (ConversionService)converterRegistry;
converterRegistry.addConverter(new ArrayToCollectionConverter(conversionService));
converterRegistry.addConverter(new CollectionToArrayConverter(conversionService));
converterRegistry.addConverter(new ArrayToArrayConverter(conversionService));
converterRegistry.addConverter(new CollectionToCollectionConverter(conversionService));
converterRegistry.addConverter(new MapToMapConverter(conversionService));
converterRegistry.addConverter(new ArrayToStringConverter(conversionService));
converterRegistry.addConverter(new StringToArrayConverter(conversionService));
converterRegistry.addConverter(new ArrayToObjectConverter(conversionService));
converterRegistry.addConverter(new ObjectToArrayConverter(conversionService));
converterRegistry.addConverter(new CollectionToStringConverter(conversionService));
converterRegistry.addConverter(new StringToCollectionConverter(conversionService));
converterRegistry.addConverter(new CollectionToObjectConverter(conversionService));
converterRegistry.addConverter(new ObjectToCollectionConverter(conversionService));
converterRegistry.addConverter(new StreamConverter(conversionService));
}
private static void addScalarConverters(ConverterRegistry converterRegistry) {
converterRegistry.addConverterFactory(new NumberToNumberConverterFactory());
converterRegistry.addConverterFactory(new StringToNumberConverterFactory());
converterRegistry.addConverter(Number.class, String.class, new ObjectToStringConverter());
converterRegistry.addConverter(new StringToCharacterConverter());
converterRegistry.addConverter(Character.class, String.class, new ObjectToStringConverter());
converterRegistry.addConverter(new NumberToCharacterConverter());
converterRegistry.addConverterFactory(new CharacterToNumberFactory());
converterRegistry.addConverter(new StringToBooleanConverter());
converterRegistry.addConverter(Boolean.class, String.class, new ObjectToStringConverter());
converterRegistry.addConverterFactory(new StringToEnumConverterFactory());
converterRegistry.addConverter(new EnumToStringConverter((ConversionService)converterRegistry));
converterRegistry.addConverterFactory(new IntegerToEnumConverterFactory());
converterRegistry.addConverter(new EnumToIntegerConverter((ConversionService)converterRegistry));
converterRegistry.addConverter(new StringToLocaleConverter());
converterRegistry.addConverter(Locale.class, String.class, new ObjectToStringConverter());
converterRegistry.addConverter(new StringToCharsetConverter());
converterRegistry.addConverter(Charset.class, String.class, new ObjectToStringConverter());
converterRegistry.addConverter(new StringToCurrencyConverter());
converterRegistry.addConverter(Currency.class, String.class, new ObjectToStringConverter());
converterRegistry.addConverter(new StringToPropertiesConverter());
converterRegistry.addConverter(new PropertiesToStringConverter());
converterRegistry.addConverter(new StringToUUIDConverter());
converterRegistry.addConverter(UUID.class, String.class, new ObjectToStringConverter());
converterRegistry.addConverter(new StringToPatternConverter());
converterRegistry.addConverter(Pattern.class, String.class, new ObjectToStringConverter());
if (KotlinDetector.isKotlinPresent()) {
converterRegistry.addConverter(new StringToRegexConverter());
converterRegistry.addConverter(Regex.class, String.class, new ObjectToStringConverter());
}
}
}
- `ConvertRegistry`에 이미 자주 사용하는 Converter가 등록되어있습니다.
상속 & 구현 구조
- ISP(인터페이스 분리 원칙, Interface Segregation Principal)가 잘 지켜진, 명확한 역할 분리
- `ConversionRegistry` : 컨버터를 등록하는 역할입니다.
- 정의 : Converter를 모아서 편리하게 관리하고 사용할 수 있게 해주는 기능을 제공합니다.
- `ConversionService` : 컨버터를 사용하는 역할입니다.
- 정의 : Converter를 등록하고 관리하는 기능을 제공합니다.
- `ConversionRegistry` : 컨버터를 등록하는 역할입니다.
- `ConversionRegistry`가 변경되어도 `ConversionService`에 지장이 없습니다.
ConversionService
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
default Object convert(@Nullable Object source, TypeDescriptor targetType) {
return this.convert(source, TypeDescriptor.forObject(source), targetType);
}
@Nullable
Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType);
}
- `canConvert(@Nullable Class<?> sourceType, Class<?> targetType)`
주어진 원본 클래스 타입에서 목표 클래스 타입으로 변환이 가능한지를 검사합니다. - `<T> T convert()`
주어진 객체를 지정된 클래스 타입으로 변환합니다.
ConverterRegistry
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);
}
- `void addConverter()`
일반적인 `Converter`를 등록합니다. - `<S, T> void addConverter`
변환할 source 타입과 target 타입을 명시적으로 지정하여 `Converter`를 등록합니다.
사용 예시
특정 파라미터에 관련된 resolver 내부에서 다음과 같이 converter 사용합니다.
conversionService.convert(String value, targetType);
- 등록된 converter 중에서, 매개 변수를 보고 타입에 맞는 converter를 자동으로 호출합니다.
- 반환 타입, 파라미터 타입, 제네릭 등으로 `ConversionService`가 converter를 찾습니다.
- converter는 `ConversionService` 내부에서 숨겨진채 제공됩니다.
- 즉, 클라이언트는 `ConversionService` 인터페이스만 의존하면 됩니다.
- 컨버터 등록돠 사용의 분리
✨ Spring은 내부적으로 `ConversionService`를 사용해 타입을 변환합니다. 대표적으로 `@RequestParam`, @PathVariable`, `@ModelAttribute` 등이 해당 기능을 사용합니다.
WebConfig로 Custom Converter 등록하는 방법
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFomatters(FormatterRegistry registry) {
// registry.addConverter(new "${등록할 Converter}");
registry.addConverter(new myCustomConverter1());
registry.addConverter(new myCustomConverter2());
}
}
- WebMvcConfigurer를 직접 구현하여, 직접 구현한 custom converter를 등록(addConverter())하면, `ConversionService`를 구현한 구현체에 등록됩니다. (예: `DefaultConversionService`)
참조 문헌
https://docs.spring.io/spring-framework/reference/core/validation/convert.html
Spring Type Conversion :: Spring Framework
When you require a sophisticated Converter implementation, consider using the GenericConverter interface. With a more flexible but less strongly typed signature than Converter, a GenericConverter supports converting between multiple source and target types
docs.spring.io