실제 사내에서 운영 중인 프로젝트에 적용했기 때문에, DB 관련 설정은 따로 하지 않았다. Spring Boot 관련 애플리케이션 설정만 했다.
local 환경에서의 설정을 예시로 가져오자면 다음과 같이 설정했다.
spring:
jpa:
properties:
hibernate:
show_sql: false
generate-ddl: false
format_sql: false
physical_naming_strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
jdbc:
lob:
non_contextual_creation: true # createClob() 메서드를 구현하지 않았다는 hibernate의 에러 로그를 보여주지 않기 위함.
hbm2ddl:
auto: none # 로컬에서 create-ddl이 필요할 경우 create로 변경
datasource:
url: jdbc:mysql://localhost:3307/
username:
password:
replica:
url: jdbc:mysql://localhost:3308/
username:
password:
실제로 DB 서버가 Writer DB, Reader DB로 나눠진 경우는 프로덕션 환경 뿐이었기 때문에, 제대로 잘 적용됐는지는 프로덕션 환경에 배포한 뒤 CPU Usage를 확인했다. 물론 로컬에서도 Read Query의 경우 Reader DB로 가는지도 로그를 통해 확인했다.
위처럼 적용하면 JPA와 관련된 설정이 먹히지 않는다. 커스텀한 Configuration 파일을 작성해 Hibernate에서 제공해주는 옵션을 임의로 박아야한다.
@ConstructorBinding
@ConfigurationProperties(prefix = "spring.datasource")
data class ReplicationDataSourceProperties(
val url: String = "",
val username: String = "",
val password: String = "",
val replica: Replica,
) {
data class Replica(
val url: String = "",
val username: String = "",
val password: String = "",
)
}
디폴트 생성자를 주기 위해 기본값을 설정했다.
@EnableConfigurationProperties(ReplicationDataSourceProperties::class, HibernateProperties::class)
@Configuration
class ReplicationDataSourceConfig(
private val dataSourceProperties: ReplicationDataSourceProperties,
private val jpaProperties: JpaProperties,
private val hibernateProperties: HibernateProperties,
) {
@Bean
fun routingDataSource(): DataSource {
val sourceDataSource = createDataSource(
url = dataSourceProperties.url,
username = dataSourceProperties.username,
password = dataSourceProperties.password,
)
val dataSources = mutableMapOf<Any, Any>()
dataSources[SOURCE] = sourceDataSource
val replica = dataSourceProperties.replica
val replicaDataSource = createDataSource(
url = replica.url,
username = replica.username,
password = replica.password,
)
dataSources[REPLICA] = replicaDataSource
val replicationRoutingDataSource = ReplicationRoutingDataSource()
replicationRoutingDataSource.setDefaultTargetDataSource(sourceDataSource)
replicationRoutingDataSource.setTargetDataSources(dataSources)
return replicationRoutingDataSource
}
fun createDataSource(
url: String,
username: String,
password: String,
): DataSource {
return DataSourceBuilder
.create()
.url(url)
.username(username)
.password(password)
.build()
}
@Bean
fun dataSource(): DataSource {
return LazyConnectionDataSourceProxy(routingDataSource())
}
@Primary
@Bean(name = ["entityManagerFactory"])
fun entityManagerFactory(
beanFactory: ConfigurableListableBeanFactory
): LocalContainerEntityManagerFactoryBean {
val entityManagerFactoryBuilder: EntityManagerFactoryBuilder =
createEntityManagerFactoryBuilder(jpaProperties)
val localContainerEntityManagerFactoryBean =
entityManagerFactoryBuilder.dataSource(dataSource())
.properties(hibernateProperties.determineHibernateProperties(jpaProperties.properties, HibernateSettings()))
.packages("com.daangn.business.platform.*")
.build()
localContainerEntityManagerFactoryBean.jpaPropertyMap[AvailableSettings.BEAN_CONTAINER] =
SpringBeanContainer(beanFactory)
return localContainerEntityManagerFactoryBean
}
private fun createEntityManagerFactoryBuilder(jpaProperties: JpaProperties): EntityManagerFactoryBuilder {
val vendorAdapter: JpaVendorAdapter = HibernateJpaVendorAdapter()
return EntityManagerFactoryBuilder(vendorAdapter, jpaProperties.properties, null)
}
// JPA에서 사용할 TransactionManager 설정
@Bean
fun transactionManager(entityManagerFactory: EntityManagerFactory): PlatformTransactionManager? {
val tm = JpaTransactionManager()
tm.entityManagerFactory = entityManagerFactory
return tm
}
}
위는 Hibernate 설정을 적용한 엔티티 매니저 팩토리를 만드는 설정과 routing할 수 있는 각각의 dataSource를 생성해주는 설정 파일이다.
여기서 주의해야할 곳이 entityManagerFactory 를 생성하는 부분이다. 대부분의 multiple-datasources 를 적용하는 블로그나 공식 문서 등을 찾아봐도 LocalContainerEntityManagerFactoryBean 을 생성할 때 hibernate properties를 따로 주입해주지 않는다.
아래처럼…
// hibernate properties를 따로 주입해주지 않는 예시
@Primary
@Bean(name = ["entityManagerFactory"])
fun entityManagerFactory(
beanFactory: ConfigurableListableBeanFactory
): LocalContainerEntityManagerFactoryBean {
val entityManagerFactoryBuilder: EntityManagerFactoryBuilder =
createEntityManagerFactoryBuilder(jpaProperties)
val localContainerEntityManagerFactoryBean =
entityManagerFactoryBuilder.dataSource(dataSource())
.packages("com.daangn.business.platform.*")
.build()
return localContainerEntityManagerFactoryBean
}
그러나, 만약 코드에서 AttributeConverter 를 사용한다면 거의 99%의 블로그에서 제시하는 대로 LocalContainerEntityManagerFactoryBean 을 생성한다면 무조건 다음과 같은 에러를 만날 것이다.
org.hibernate.AnnotationException: Unable to instantiate AttributeConverter
해당 에러로 검색하면 이 블로그 글을 가장 먼저 만나볼 수 있다.
https://brunch.co.kr/@purpledev/33
이 블로그에서 제시하는 해결책은 LocalContainerEntityManagerFactoryBean 을 통해 entityManager 생성 시 bean을 주입받을 수 있도록 BEAN_CONTAINER 속성을 jpaPropertyMap 에 주입하는 것이다.
@Primary
@Bean(name = ["entityManagerFactory"])
fun entityManagerFactory(
beanFactory: ConfigurableListableBeanFactory
): LocalContainerEntityManagerFactoryBean {
val entityManagerFactoryBuilder: EntityManagerFactoryBuilder =
createEntityManagerFactoryBuilder(jpaProperties)
val localContainerEntityManagerFactoryBean =
entityManagerFactoryBuilder.dataSource(dataSource())
.packages("com.daangn.business.platform.*")
.build()
// 추가된 부분
localContainerEntityManagerFactoryBean.jpaPropertyMap[AvailableSettings.BEAN_CONTAINER] = SpringBeanContainer(beanFactory)
return localContainerEntityManagerFactoryBean
}
하지만 난 이렇게 해도 AttributeConverterDefinition 에서 AttributeConverter 를 초기화할 수 없다는 에러가 다시 발생했다.
org.hibernate.cfg.AttributeConverterDefinition.instantiateAttributeConverter(AttributeConverterDefinition.java:60)
해당 클래스는 Deprecated 된 클래스인데, 왜 저 코드라인을 실행하는지 의문이었다. 브레이크 포인트를 걸고 기존과 달라진 부분을 찾아보았다.
첫번째로 발견한 부분은 Scanner 였다.
1도 이해가 안되지만 Scanner docs 를 참고하면 아래처럼 설명돼있다.
Persistence 단위 내부의 클래스, 패키지 및 리소스를 검색할 수 있도록 Hibernate에 대한 Contract를 정의합니다.
Replication을 적용하기 전에는 즉 SingleDataSource의 경우에는 JpaProperties와 HibernateProperties가 함께 적용된다. 이 때 HibernateProperties에 설정된 DISABLED_SCANNER_CLASS가 자동으로 설정된다.
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(HibernateProperties.class)
@ConditionalOnSingleCandidate(DataSource.class)
class HibernateJpaConfiguration extends JpaBaseConfiguration {
}
하지만 지금처럼 멀티소스인 경우에는 해당 HibernateJpaConfiguration이 자동으로 설정되지 않기 때문에 HibernateProperties가 먹히지 않고, Scanner Class가 Disabled가 아닌 다른 스캐너로 먹힌다.
다른 스캐너 + ScanSettings가 null이기 때문에 적절한 Scan 행위를 하지 못하고, 따라서 AttributeConverter의 생성자를 찾지 못해 init 을 하지 못하는 것!
아무튼 말이 길었는데 결론적으로 위에서 언급한 대로 설정해주면 제대로 동작한다.
@Primary
@Bean(name = ["entityManagerFactory"])
fun entityManagerFactory(
beanFactory: ConfigurableListableBeanFactory
): LocalContainerEntityManagerFactoryBean {
val entityManagerFactoryBuilder: EntityManagerFactoryBuilder =
createEntityManagerFactoryBuilder(jpaProperties)
val localContainerEntityManagerFactoryBean =
entityManagerFactoryBuilder.dataSource(dataSource())
// 여기가 핵심 1
.properties(hibernateProperties.determineHibernateProperties(jpaProperties.properties, HibernateSettings()))
.packages("com.daangn.business.platform.*")
.build()
// 여기가 핵심 2
localContainerEntityManagerFactoryBean.jpaPropertyMap[AvailableSettings.BEAN_CONTAINER] =
SpringBeanContainer(beanFactory)
return localContainerEntityManagerFactoryBean
}
공감하며 읽었습니다. 좋은 글 감사드립니다.