본문 바로가기

Programing/Framework

[SpringBoot] ConversionService에 대한 오해

부제: spring-integration 를 추가하면서 발행한 오류를 통한 ConversionService에 대한 추적

spring-boot 프로젝트에서 "org.springframework.boot:spring-boot-starter-web"를 사용하고 있었다.
엔드포인트로 TCP 연결이 필요해서 스프링 통합 프로젝트의 Spring Integration IP Support를 사용하기 위해 "org.springframework.integration:spring-integration-ip" 의존성을 추가하였다.

잘 돌아가는 애플리케이션이 시작이 되지 않는다. 메세지는 아래와 같다.

Description: Parameter 2 of constructor in com.tistory.repository.CacheSupplier required a single bean, but 2 were found:
- integrationConversionService: defined in null
- mvcConversionService: defined by method 'mvcConversionService' in class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class] Action: Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed

CacheSupplier 라는 빈을 생성하기 위해 의존하고 있는 org.springframework.core.convert.ConversionService 을 주입받아야 하는데, 해당 인터페이스를 구현하는 빈이 2개 이상이라 정하기 어렵다라는 많이 보던 패턴이다.
위의 메세지처럼 @Qualifier 로 이름을 명시적으로 설정하거나, 빈에 @Primary로 우선순위로 정해주면 된다.

mvcConversionService

spring-boot-starter-web에 의존을 하면 기본으로 만들어주는 빈이다.
org.springframework.boot.autoconfigure.BackgroundPreinitializer 클래스의  performPreinitialization 메서드에는 ConversionServiceInitializer 를 수행하도록 되어 있다.

private void performPreinitialization() {
	try {
		Thread thread = new Thread(new Runnable() {

			@Override
			public void run() {
				runSafely(new ConversionServiceInitializer());
				runSafely(new ValidationInitializer());
				runSafely(new MessageConverterInitializer());
				runSafely(new MBeanFactoryInitializer());
				runSafely(new JacksonInitializer());
				runSafely(new CharsetInitializer());
				preinitializationComplete.countDown();
			}
		// ...
}

하지만 mvcConversionService 라는 이름은 어디에 있는 걸까?

org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration 설정 중에 EnableWebMvcConfiguration 라는 정적 중첩 클래스가 있는데 여기에 아래와 같은 코드가 있다.

@Bean
@Override
public FormattingConversionService mvcConversionService() {
	WebConversionService conversionService = new WebConversionService(
			this.mvcProperties.getDateFormat());
	addFormatters(conversionService);
	return conversionService;
}

바로 FormattingConversionService 가 ConversionService 인터페이스를 구현한 구현체인 것이다.

integrationConversionService

integrationConversionService 는 뭘까? 앞에 integration이 붙어서 알겠지만 spring integration 에서 사용하는 ConversionService로 보인다. 이 빈의 등록은 IntegrationRegistrar의 registerBeanDefinitions에서 수행하며 registerBuiltInBeans(registry); 실행 시 등록된다.

public class IntegrationRegistrar implements ImportBeanDefinitionRegistrar, BeanClassLoaderAware {

	private final static IntegrationConverterInitializer INTEGRATION_CONVERTER_INITIALIZER =
				new IntegrationConverterInitializer();
	            
	private void registerBuiltInBeans(BeanDefinitionRegistry registry) {
		// ...
		if (!alreadyRegistered && !registriesProcessed.contains(registryId) && JacksonPresent.isJackson2Present()) {
			registry.registerBeanDefinition(
					IntegrationContextUtils.TO_STRING_FRIENDLY_JSON_NODE_TO_STRING_CONVERTER_BEAN_NAME,
					BeanDefinitionBuilder.genericBeanDefinition(IntegrationConfigUtils.BASE_PACKAGE +
							".json.ToStringFriendlyJsonNodeToStringConverter")
							.getBeanDefinition());
			INTEGRATION_CONVERTER_INITIALIZER.registerConverter(registry,
					new RuntimeBeanReference(
							IntegrationContextUtils.TO_STRING_FRIENDLY_JSON_NODE_TO_STRING_CONVERTER_BEAN_NAME));
		}

		registriesProcessed.add(registryId);
	}
INTEGRATION_CONVERTER_INITIALIZER.registerConverter(registry, new RuntimeBeanReference( IntegrationContextUtils.TO_STRING_FRIENDLY_JSON_NODE_TO_STRING_CONVERTER_BEAN_NAME));

결국 IntegrationConverterInitializer#registerConverter 에 의해 수행되며 integrationConversionService 이름은 IntegrationUtils에 정의되어 있다.

public final class IntegrationUtils {
	// ...
	public static final String INTEGRATION_CONVERSION_SERVICE_BEAN_NAME = "integrationConversionService";

하지만 이 빈은 null이다.

해결책은?

stackoverflow에 같은 질문이 있다.
https://stackoverflow.com/questions/28111599/multiple-conversionservices-in-spring-boot

 

Multiple conversionServices in spring-boot

I have a boot application and in one of my facades I try to Autowire the conversionService like this: @Autowired private ConversionService conversionService; as a result I get this: Caused by: org.

stackoverflow.com

답변은 conversionService 를 직접 설정을 해주던지(타입에 의한 주입이 아닌 명시적인 이름에 의한 주입을 하게 수정하던지,
명시적으로 mvcConversionService으로 대상을 정해주던지 하면 된다.

@Bean(name="conversionService")
public ConversionService getConversionService() {
    ConversionServiceFactoryBean bean = new ConversionServiceFactoryBean();
    bean.afterPropertiesSet();
    return bean.getObject();
}
// 또는
@Qualifier("mvcConversionService") ConversionService conversionService

프레임워크를 사용하다 문제가 발생시 해결을 위해서는 프레임워크의 내부의 구현에 대해 알 필요가 가끔 생긴다.