Spring

[Spring] 파라미터 변환을 위한 Convert 생성 및 등록 방법

kimyongjun0129 2025. 6. 8. 00:50

 


 

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);
    }
}
  1. 컨트롤러 메서드를 통해 Http 요청을 받습니다.
  2. HttpServletRequest 객체를 통해 Parameter 값을 받습니다.
  3. Key가 "example"인 Parameter의 Value 값을 가져옵니다.
    • Parameter의 Value 값은 항상 String입니다.
  4. 문자열을 정수형으로 바꿔주기 위해 `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)가 잘 지켜진, 명확한 역할 분리
    1. `ConversionRegistry` : 컨버터를 등록하는 역할입니다.
      • 정의 : Converter를 모아서 편리하게 관리하고 사용할 수 있게 해주는 기능을 제공합니다.
    2.  `ConversionService` : 컨버터를 사용하는 역할입니다.
      • 정의 : Converter를 등록하고 관리하는 기능을 제공합니다.
  • `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