로컬 테스트 환경 만드느라 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하는 곳에 두지 않는 것이 정신건간에 이로움