Spring Converters

박주진·2021년 8월 22일
0

스프링을 사용하다 보면 종종 api를 통해 요청되는 값을 원하는 객체나 특정 타입으로 변환해서 controller에서 받고 싶을 때가 있습니다. 그때 커스텀 Converter를 등록하면 원하는 타입으로 변환할 수 있습니다. 스프링에서는 커스텀 Converter 구현시 유용하게 사용할 수 있는 3가지 인터페이스 Converter, ConverterFactory, GenericConverter를 제공합니다.

3가지 인터페이스의 차이는?

스프링 공식문서를 여러 번 읽어 봤지만 잘 이해가 되지 않습니다. 그래도 이해한 내용을 대략적으로 정리해 보면 다음과 같습니다.

  • Converter
    가장 단순하고 구체적인 타입 변환을 위해 사용한다. 예) String -> Integer
  • ConverterFactory
    클래스 계층에 전부에 대한 변환 로직이 필요할 때 사용한다. 예) String -> Enum
  • GenericConverter
    세밀한 변환 로직이 필요할 때 사용한다. field의 annotation 또는 field의 generic 정보를 기반으로 변환이 가능하다. 예) Array -> List

저의 나름대로의 결론은 source, target 타입 모두 generic 정보 없이 변환할 수 있다면 Converter 아닐 경우 ConverterFactory 또는 GenericConverter를 고려하면 좋을 것 같습니다.

어떻게 converter를 등록할 수 있을까?

우리는 아래와 같은 방법으로 custom converter를 등록해서 사용할 수 있습니다.

    @Configuration
    public class WebConfig implements WebMvcConfigurer {

        @Override
        public void addFormatters(FormatterRegistry registry) {
           // GenericConverter 등록시
            GenericConverter converter = new CustomConverter() 
            registry.addConverter(converter);
            
            // Converter 등록시
            Converter converter = new CustomConverter() 
            registry.addConverter(converter);
            
            // ConverterFactory 등록시
            ConverterFactory converter = new CustomConverterFactory() 
            registry.addConverterFactory(converter);
        }
    }

어떻게 converter를 사용해서 타입이 변환이 될까?

디버깅을 통해 쫓아가보니 controller에서 타입 변환을 GenericConversionService라는 객체가 담당하는 것을 알 수 있었습니다.

그런데 🤚🏻 잠깐! 다시 위로 돌아가 등록하는 방법을 살펴보니 구현한 Converter 인터페이스에 따라 다른 방법으로 등록을 해야 한다는 점을 발견했습니다. 그럼 GenericConversionService는 3가지의 다른 형태의 Converter들을 관리해야 하는 걸까?🤔 라는 궁금증이 들었습니다.
그래서 찾아보니 GenericConversionService는 GenericConverter 타입 하나로 관리한다는 걸 알 수 있었습니다.

그럼 어떻게🤔 공통 인터페이스를 구현하지 않는 3가지의 다른 인터페이스를 GenericConversionService에서는 처리하는 걸까?🤔
해답은 아래 코드처럼 adapter 패턴에 있었습니다.

	@Override
	public void addConverter(Converter<?, ?> converter) {
 		//중략
		addConverter(new ConverterAdapter(converter, typeInfo[0], typeInfo[1]));
	}

	@Override
	public void addConverterFactory(ConverterFactory<?, ?> factory) {
 		//중략
		addConverter(new ConverterFactoryAdapter(factory,
				new ConvertiblePair(typeInfo[0].toClass(), typeInfo[1].toClass())));
	}

	@Override
	public void addConverter(GenericConverter converter) {
		this.converters.add(converter);
		invalidateCache();
	}

그럼 다시 본론으로 돌아와 GenericConversionService는 어떻게 타입 변환을 진행하는 건지 알아봅시다. 하지만 그전에 GenericConversionService의 공개 api를 간략하게 정리해보면 아래와 같습니다.


(1) if (genericConversionService는.canConvert(sourceType,targetType)){
	(2) Object convertedObject=genericConversionService는.convert(source, sourceType, targetType);
}
1. canConvert 메서드로 변환이 가능한지 확인한다.
2. convert 메서드를 통해 변화을 진행한다.

아래 코드를 통해 두메서드를 자세히 들여다보면 핵심은 getConverter() 라는 메서드라는 걸 알 수 있습니다.

	public boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
		//중략
		GenericConverter converter = getConverter(sourceType, targetType);
		return (converter != null);
	}


	public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
		//중략
		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);
	}

드디어 getConverter() 라는 메서드에 제가 찾던 변환 로직이 들어 있었습니다.😀
자세한 내용은 아래와 같습니다.

	protected GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) {
		ConverterCacheKey key = new ConverterCacheKey(sourceType, targetType);
		
       		***(1) GenericConverter converter = this.converterCache.get(key);
		if (converter != null) {
			return (converter != NO_MATCH ? converter : null);
		}

		***(2) converter = this.converters.find(sourceType, targetType);
		//중략
	}
    
   public GenericConverter find(TypeDescriptor sourceType, TypeDescriptor targetType) {
			// Search the full type hierarchy
            
            	*****(3)
			List<Class<?>> sourceCandidates = getClassHierarchy(sourceType.getType());
			List<Class<?>> targetCandidates = getClassHierarchy(targetType.getType());
			for (Class<?> sourceCandidate : sourceCandidates) {
				for (Class<?> targetCandidate : targetCandidates) {
					ConvertiblePair convertiblePair = new ConvertiblePair(sourceCandidate, targetCandidate);
					GenericConverter converter = getRegisteredConverter(sourceType, targetType, convertiblePair);
					if (converter != null) {
						return converter;
					}
				}
			}
			return null;
	}
    
    1. cache에서 변환할 수 있는 converter가 있는지 확인한다. 있으면 이를 활용한다.
    2. cache에서 찾지 못했다면 내부에 등록된 converters 중에서 찾는다.
    3. 변환할 source 타입과 변환될 target 타입의 전체 클래스 계층을 뒤져 변환이 가능한 converter를 찾는다.
    예) String -> Integer
    List<Class<?>> sourceCandidates = (class java.lang.String, interface java.io.Serializable, interface.java.lang.Comparable, interface java.lang.CharSequence, class java.lang.Object);
    List<Class<?>> targetCandidates = (class java.lang.integer, class java.lang.Number, interface java.lang.Comparable, interface java.io.Serializable,  class java.lang.Object)
    class java.lang.String -> class java.lang.integer 으로 변환 가능한 converter 찾기 
    class java.lang.String -> class java.lang.Number 으로 변환 가능한 converter 찾기
    class java.lang.String -> interface java.lang.Comparable 으로 변환 가능한 converter 찾기
    .
    .
    .
    class java.lang.Object -> class java.lang.Object 까지 찾을때 까지 모든 조합으로 시도해본다.

실제 소스는 너무 길어 생략한 getRegisteredConverter() 메서드에 대한 설명을 덧붙이자면 ConcurrentHashMap에서 ConvertiblePair라는 객체를 key로 특정 converter를 찾는 방식입니다. GenericConversionService는 모든 converter들을 ConvertiblePair라는 객체를 key로 ConcurrentHashMap에 저장해두기 때문에 빠르게 찾아올 수 있습니다.
참고로 ConvertiblePair라는 객체는 source class,target class 이 두 가지 타입을 감싼 객체로 생각하시면 될 것 같습니다.

0개의 댓글