2024년 8월 23일 금요일

Caused by: org.hibernate.AssertionFailure: AttributeConverter class [xxxConverter] registered multiple times 문제 해결

로컬 테스트 환경 만드느라 test에 entityManagerFactory 만들 기회가 있었음.

    @Bean
    @Primary
    fun entityManagerFactory(): LocalContainerEntityManagerFactoryBean {
        return LocalContainerEntityManagerFactoryBean().apply {
            dataSource = masterDataSource(masterHikariConfig())
            setPackagesToScan("com.entity")
            jpaVendorAdapter = HibernateJpaVendorAdapter()

            setJpaProperties(object : Properties() {
                init {
                    put("hibernate.dialect", "org.hibernate.dialect.H2Dialect")
                    put("hibernate.hbm2ddl.auto", "create")
                    put("hibernate.show_sql", "true")
                }
            })
            afterPropertiesSet()
        }
    }
근데 계속 에러남.

Caused by: org.hibernate.AssertionFailure: AttributeConverter class [xxxConverter] registered multiple times
AttributeConverter가 이미 등록되어 있다는 오류임 Hibernate 코드를 보면

org.hibernate.boot.model.convert.internal.AttributeConverterManager.java
...
		if ( old != null ) {
			throw new AssertionFailure(
					String.format(
							Locale.ENGLISH,
							"AttributeConverter class [%s] registered multiple times",
							descriptor.getAttributeConverterClass()
					)
			);
		}
...        
요기에서 에러를 내는 것임. 왜 이게 나나 봤더니

org.hibernate.boot.model.process.spi.MetadataBuildingProcess.java 

	public static MetadataImplementor complete(
			final ManagedResources managedResources,
			final BootstrapContext bootstrapContext,
			final MetadataBuildingOptions options) {
            ...
            managedResources.getAttributeConverterDescriptors().forEach( metadataCollector::addAttributeConverter );
            ...
            
	final MetadataSourceProcessor processor = new MetadataSourceProcessor() {
			private final MetadataSourceProcessor hbmProcessor =
						options.isXmlMappingEnabled()
							? new HbmMetadataSourceProcessorImpl( managedResources, rootMetadataBuildingContext )
							: new NoOpMetadataSourceProcessorImpl();

			private final AnnotationMetadataSourceProcessorImpl annotationProcessor = new AnnotationMetadataSourceProcessorImpl(
					managedResources,
					rootMetadataBuildingContext,
					jandexView
			);  
     ...       
            
}            

metadataCollector::addAttributeConverter
첫번째는 package scan을 하여 Converter를 수집하고

org.hibernate.boot.model.source.internal.annotations.AnnotationMetadataSourceProcessorImpl.java

private void categorizeAnnotatedClass(Class annotatedClass, AttributeConverterManager attributeConverterManager) {
		final XClass xClass = reflectionManager.toXClass( annotatedClass );
		// categorize it, based on assumption it does not fall into multiple categories
		if ( xClass.isAnnotationPresent( Converter.class ) ) {
			//noinspection unchecked
			attributeConverterManager.addAttributeConverter( (Class>) annotatedClass );
		}
		else if ( xClass.isAnnotationPresent( Entity.class )
				|| xClass.isAnnotationPresent( MappedSuperclass.class ) ) {
			xClasses.add( xClass );
		}
		else if ( xClass.isAnnotationPresent( Embeddable.class ) ) {
			xClasses.add( xClass );
		}
		else {
			log.debugf( "Encountered a non-categorized annotated class [%s]; ignoring", annotatedClass.getName() );
		}
	}

여기서 Annotation을 스캔해서 한번 찾아 넣음. 두번째는 @Converter 요걸 찾아 넣는 것임. 그래서 설정을 잘못하면 2번 AttributeConverterManager에 들어가고 에러가 나는 것임. 어떻게 찾았냐면. org.hibernate.boot.model.process.spi.MetadataBuildingProcess.class가 데이터를 만들기 위해 사전작업을 하는데

org.hibernate.boot.model.process.spi.MetadataBuildingProcess.java
...
	public static ManagedResources prepare(
			final MetadataSources sources,
			final BootstrapContext bootstrapContext) {
		final ManagedResourcesImpl managedResources = ManagedResourcesImpl.baseline( sources, bootstrapContext );
		final ConfigurationService configService = bootstrapContext.getServiceRegistry().getService( ConfigurationService.class );
		final boolean xmlMappingEnabled = configService.getSetting(
				AvailableSettings.XML_MAPPING_ENABLED,
				StandardConverters.BOOLEAN,
				true
		);
		ScanningCoordinator.INSTANCE.coordinateScan(
				managedResources,
				bootstrapContext,
				xmlMappingEnabled ? sources.getXmlMappingBinderAccess() : null
		);
		return managedResources;
	}
ScanningCoordinator.INSTANCE.coordinateScan을 하게 됨. 여기를 자세히 보면

org.hibernate.boot.model.process.internal.ScanningCoordinator.java

		for ( ClassDescriptor classDescriptor : scanResult.getLocatedClasses() ) {
			if ( classDescriptor.getCategorization() == ClassDescriptor.Categorization.CONVERTER ) {
				// converter classes are safe to load because we never enhance them,
				// and notice we use the ClassLoaderService specifically, not the temp ClassLoader (if any)
				managedResources.addAttributeConverterDefinition(
						new ClassBasedConverterDescriptor(
								classLoaderService.classForName( classDescriptor.getName() ),
								bootstrapContext.getClassmateContext()
						)
				);
			}
			else if ( classDescriptor.getCategorization() == ClassDescriptor.Categorization.MODEL ) {
				managedResources.addAnnotatedClassName( classDescriptor.getName() );
			}
			unresolvedListedClassNames.remove( classDescriptor.getName() );
		}
이렇게 scanResult로 대상이 있으면 CONVERTER를 addAttributeConverterDefinition에 등록하게 됨. 즉 2번 들어가면 저 scanResult에서 제외를 시켜야 하는 것임. 그럼 저 scanResult는 어디서 가져오냐면

		final ScanResult scanResult = scanner.scan(
				bootstrapContext.getScanEnvironment(),
				bootstrapContext.getScanOptions(),
				StandardScanParameters.INSTANCE
		);
스캐너가 도는데 저 스캐너의 범위는 처음 entityManagerFactory가 설정한 setPackagesToScan() 안의 3가지 선언된 Categorization Class를 스캔함

public interface ClassDescriptor {
	enum Categorization {
		MODEL,
		CONVERTER,
		OTHER
	}
    ...

}
즉 스캔을 한 entity package 하위에 @Converter Annotation을 두게 되면 스캔을 할 때 같이 잡혀서 자동으로 등록되고 Springboot의 Component Scan에도 @Converter를 잡게되면 2개가 등록됨. Converter는 entity를 scan하는 곳에 두지 않는 것이 정신건간에 이로움