본문 바로가기

Programing/Framework

[Spring] core - Converter 인터페이스

 

org.springframework.core.convert.converter 패키지에는 Converter라는 이름의 인터페이스가 있다.

@FunctionalInterface
public interface Converter<S, T> {

	/**
	 * Convert the source object of type {@code S} to target type {@code T}.
	 * @param source the source object to convert, which must be an instance of {@code S} (never {@code null})
	 * @return the converted object, which must be an instance of {@code T} (potentially {@code null})
	 * @throws IllegalArgumentException if the source cannot be converted to the desired target type
	 */
	@Nullable
	T convert(S source);

}

변환을 하는 용도는 확실한데 어떻게 써야할지 막연하다. 막연하다기 보다는 이 인터페이스를 쓰지 않고 바로 컨버터를 만들어도 되는데 구지 구현을 해서 얻을 수 있는 장점이 무엇인지가 궁금하다.

 

스프링에서 구현되어 있는 예를 살펴보기로 하였다.

 

EnumToStringConverter

enum 타입을 문자열로 바꾸는 EnumToStringConverter를 살펴보자.

final class EnumToStringConverter extends AbstractConditionalEnumConverter implements Converter<Enum<?>, String> {

	public EnumToStringConverter(ConversionService conversionService) {
		super(conversionService);
	}

	@Override
	public String convert(Enum<?> source) {
		return source.name();
	}

}

enum 타입은 name() 이라는 메서드를 가지고 있다. 이 메서드를 호출하게 구현이 되어 있다. (toString()도 같지 않냐고 하는 사람이 있다면 차이에 대해 공부할 필요가 있다.)

시그너처를 보면 단순 Converter 인터페이스만 구현한 것이 아니고 ConditionalConverter를 구현한 AbstractConditionalEnumConverter 추상 클래스를 상속받고 있음을 알 수 있다.

abstract class AbstractConditionalEnumConverter implements ConditionalConverter {

	private final ConversionService conversionService;


	protected AbstractConditionalEnumConverter(ConversionService conversionService) {
		this.conversionService = conversionService;
	}


	@Override
	public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
		for (Class<?> interfaceType : ClassUtils.getAllInterfacesForClassAsSet(sourceType.getType())) {
			if (this.conversionService.canConvert(TypeDescriptor.valueOf(interfaceType), targetType)) {
				return false;
			}
		}
		return true;
	}

}

소스와 대상을 TypeDescriptor 타입으로 받는 matches라는 메서드 인터페이스를 구현하고 있다.

생성자에서 ConversionService 를 주입받고 있다.

그래서 EnumToStringConverter 역시 ConversionService 를 주입해야 한다.

 

DefaultConversionService

이 EnumToStringConverter는 DefaultConversionService 에서 사용이 되고 있다.

DefaultConversionService의 계층구조

이 클래스의 인터페이스를 거슬러 올라가면 ConverterRegistry 인터페이스가 있다.

이 인터페이스에는 Converter를 추가할 수 있는 addConverter 메서드를 가지고 있다.

public interface ConverterRegistry {

	void addConverter(Converter<?, ?> converter);
    // ...

사실 Registry에 어떻게 등록을 될지는 걱정할 필요가 없다.

왜냐하면 Converter 인터페이스를 구현하는 빈이라면 스프링이 알아서 자동으로 등록을 해주기 때문이다.

 

Converter 인터페이스를 스프링에 등록하기

예제로 아래와 같은 DTO가 있다고 가정한다. (lombok을 썼다.)

@Getter
@Setter
public class FooDto {
    @NotBlank(message = "우편번호를 작성해주세요.")
    @Pattern(regexp = "[0-9]{5}", message = "5자리의 숫자만 입력가능합니다")
    private String zipcode;
}

위 FooDto를 String 타입으로 바꿔야 할 필요가 있다고 할 때 아래와 같이 converer를 구현할 수 있다.

public class FooDtoConverter implements Converter<FooDto, String> {
    @Override
    public String convert(FooDto source) {
        return source.getZipcode();
    }
}

그러면 어떻게 해야 FooDtoConverter를 프레임워크에 등록할 수 있을까?

@Configuration을 통한 빈 생성

아래와 같은 설정 클래스를 이용하면 FooDtoConverter 타입의 Bean을 등록할 수 있다.

@Configuration
public class ConverterConfig {
    @Bean
    public FooDtoConverter fooDtoConverter() {
        return new FooDtoConverter();
    }
}

FooDtoConverter에 @Component 붙이기

위에서 만들었던 FooDtoConverter 클래스에 @Component 애너테이션을 붙여도 Bean으로 등록이 된다.
(@Repository 나 @Service, @Controller 중 어느 것을 붙여도 무방하나 리포지토리도 아니고 서비스도 아니고, 컨트롤러도 아니라서 그냥 컴포넌트가 적당하다.)

@Component
public class FooDtoConverter implements Converter<FooDto, String> {
    @Override
    public String convert(FooDto source) {
        return source.getZipcode();
    }
}

커스텀  Converter를 사용하기

사용 방법은 스프링 프레임워크 레퍼런스 문서에 잘 나와 있다.

단순히 ConversionService 인터페이스를 주입받아서 convert 메서드를 호출하면 된다.

다만, 이 방법은 사용하는 코드가 Bean일 경우에만 가능하다.

 

@Controller 에서 사용하는 코드로 예를 들어보겠다. (스프링 샘플로 자주 보는 HelloController)

@RestController
public class HelloController {

    private final ConversionService conversionService;

    @Autowired
    public HelloController(ConversionService conversionService) {
        this.conversionService = conversionService;
    }

    @RequestMapping("/")
    public String hello(@RequestBody(required=false) @Valid FooDto fooDto) {
        if (fooDto == null) {
            return "Need FooDto";
        }
        return "World: " + conversionService.convert(fooDto, String.class);
    }
}

ConversionService.converter에 변환할 대상과 target 객체 타입을 넘겨주면 스프링이 내부에 등록된 converter를 찾아서 변환을 해준다.

DI 프레임워크의 효과를 다시 한번 느끼게 되었다. (decoupling)

내부에서 사용은?

ConversionService.converter() 가 호출 되면 실제 GenericConversionService 의 converter가 호출된다.

public class GenericConversionService implements ConfigurableConversionService {
	// ...
	@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);
	}
	// ...

결국 내부에서 ConversionUtils.invokeConverter() 정적 메서드에 의해 구현체의 convert 메서드가 호출됨을 알 수 있다.

abstract class ConversionUtils {

	@Nullable
	public static Object invokeConverter(GenericConverter converter, @Nullable Object source,
			TypeDescriptor sourceType, TypeDescriptor targetType) {

		try {
			return converter.convert(source, sourceType, targetType);
		}
		catch (ConversionFailedException ex) {
			throw ex;
		}
		catch (Throwable ex) {
			throw new ConversionFailedException(sourceType, targetType, source, ex);
		}
	}
    // ...

만약 적절한 Converter를 찾지 못했다면???

GenericConversionService 클래스의 handleConverterNotFound(...) 메서드에 의해 처리가 되고 ConverterNotFoundException 예외가 발생할 수 있다. 인자가 조건에 맞지 않을 경우 ConversionFailedException 예외가 발생할 수도 있다.

@Nullable
private Object handleConverterNotFound(
		@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {

	if (source == null) {
		assertNotPrimitiveTargetType(sourceType, targetType);
		return null;
	}
	if ((sourceType == null || sourceType.isAssignableTo(targetType)) &&
			targetType.getObjectType().isInstance(source)) {
		return source;
	}
	throw new ConverterNotFoundException(sourceType, targetType);
}

스프링 프레임워크를 쓰지 않는다면

이 글의 설명이 스프링 프레임워크에 의존을 하고 있어 만약 스프링 프레임워크를 쓰지 않는다면 어떻게 컨버터를 만들까?

 

다른 대안으로는 MapStruct, JMapper, Orika, ModelMapper, Dozer 등의 매퍼를 이용하면 유사한 기능을 구현할 수 있을 것이다.
https://www.baeldung.com/java-performance-mapping-frameworks

 

Performance of Java Mapping Frameworks | Baeldung

A practical overview of Java mapping frameworks and their performance.

www.baeldung.com

 

다만 이번 글을 쓰게 된 계기가 기존에 MapStruct의 안좋은 점들 때문에 걷어내는 과정에서 대안을 찾다가 정리한 것이라 매퍼가 정말로 필요한지는 고민이 필요할 것이다.