Springboot 에 Jpa 를 설정할 때 개발자는 db 관련 정보만 적어주면 single-datasource 환경에 한하여 필요한 모든 구성을 자동(auto configuration)으로 설정해줍니다. 비지니스가 복잡해지면 multi-datasource 연결이 요구되어 Jpa 관련 설정을 직접 해야 하는 경우가 생길 수 있는데요, 이 글에서는 설정에 필요한 요소들과 multi-datasource 환경에서 @Transactional 이 어떤 커넥션을 획득하여 트랜잭션을 처리하는지 간단히 정리해보겠습니다.
DataSource
DataSource 는 db config 정보를 통해 커넥션을 생성하고 관리하는 객체입니다.
val config = HikariConfig().apply {
jdbcUrl = "jdbc:h2:mem:testdb"
username = "sa"
password = ""
maximumPoolSize = 100
}
val datasource = HikariDataSource(config)
위 코드는 HikariConfig 정보를 바탕으로 DataSource 를 생성하는 코드입니다. 참고로 HikariDataSource 는 생성될 때 어플리케이션에서 사용할 Connection pool 도 함께 생성합니다.
EntityManager 와 EntityManagerFactory
EntityManager 는 엔티티를 관리하는 객체로 엔티티를 저장/조회 등의 작업을 합니다. EntityManager 는 EntityManagerFactory 로 생성할 수 있는데요, EntityManagerFactory 는 Thread safe 하고 생성하는 비용도 크므로 일반적으로 어플리케이션에서 공유하여 사용합니다. 반면 EntityManager 는 생성비용도 저렴하고 Thread safe 하지 않으므로 일반적으로는 요청 당 하나씩 할당되도록 합니다. EntityManagerFactory 를 생성할 때는 아래처럼 어떤 DataSource 를 사용하여 커넥션을 획득할지를 명시해주어야 합니다.
fun entityManagerFactory(
dataSource: DataSrouce
): LocalContainerEntityManagerFactoryBean {
return builder
.dataSource(dataSource)
.packages(BASE_PACKAGES)
.persistenceUnit("entityManager")
.build()
}
이렇게 생성된 EntityManagerFactory 로 생성한 EntityManager 는 당연히 EntityManagerFactory 를 생성할 때 설정한 DataSource 를 이용하여 커넥션을 획득하게 됩니다.
TransactionManager
Jpa, Jdbc 등 DB 를 접근할 수 있는 라이브러리가 다양한 만큼 각 라이브러리에서 트랜잭션을 처리하는 방식도 다양합니다. 스프링은 TransactionManager 라는 추상화 단계를 두어 이를 구현한 라이브러리들은 동일한 방법으로 트랜잭션을 처리할 수 있게 했습니다. Jpa 에서는 JpaTransactionManager 를 이용할 수 있습니다.
@Bean
fun transactionManager(
@Qualifier("entityManagerFactory")
entityManagerFactory: EntityManagerFactory
): PlatformTransactionManager {
return JpaTransactionManager(entityManagerFactory)
}
위에서 언급한 객체를 직접 구현하면 auto-configuration 에 의존하지 않고 Jpa 를 사용할 수 있는 환경이 갖추어집니다. 아래는 위 객체를 모두 구현한 간단한 예시입니다.
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
entityManagerFactoryRef = "subEntityManagerFactory",
transactionManagerRef = "subTransactionManager",
basePackages = [SubConstants.BASE_PACKAGES],
)
class SubDatabaseConfig {
@ConfigurationProperties(prefix = "spring.sub-datasource")
@Bean("subDataSource")
fun subDataSource(): DataSource {
return DataSourceBuilder.create().type(HikariDataSource::class.java).build()
}
@Bean("subEntityManagerFactory")
fun subEntityManagerFactory(
builder: EntityManagerFactoryBuilder,
): LocalContainerEntityManagerFactoryBean {
val build = builder
.dataSource(subDataSource())
.packages(SubConstants.BASE_PACKAGES)
.persistenceUnit("subEntityManager")
.build()
return build
}
@Bean
fun subTransactionManager(
@Qualifier("subEntityManagerFactory")
subEntityManagerFactory: EntityManagerFactory
): PlatformTransactionManager {
return JpaTransactionManager(subEntityManagerFactory)
}
}
Multi-datasource 에서의 @Transactional
어떤 클래스에 @Transactional 이 하나라도 붙어있으면 실제 객체가 아닌 프록시 객체가 스프링 컨테이너에 등록됩니다. 만약 @Transactional 이 붙은 메서드를 실행하면 트랜잭션 적용 대상임을 확인하고 트랜잭션 처리를 해주고, 붙어있지 않으면 기존 메서드만 실행해줍니다.
@Transactional 에는 어떤 TransactionManager 를 사용할건지 명시해줄 수 있는데요, EntityManagerFactory 가 하나라면 TransactionManager 도 하나이므로 별다른 신경을 쓰지 않아도 됩니다. 하지만 multi-datasource 인 경우에는 신경써야할 부분이 생길 수 있습니다.
서로 다른 DataSource 가 서로 다른 Repository 를 이용하는 경우
바라 보는 데이터베이스가 완전히 다른 구성일 경우는 간단합니다. 예를 들어, 한 어플리케이션에서 payment db 와 account db 를 사용해야 한다면 각각의 db 정보를 매핑할 Entity 와 Repository 객체도 달라집니다. 따라서 단순히 설정을 따로 만들고 @Transactional 에 필요한 TransactionManager 를 넣어주면 됩니다.
두 db 를 하나의 트랜잭션으로 묶어 처리하는 것은 고려하지 않습니다.
@EnableJpaRepositories(
entityManagerFactoryRef = "accountEntityManagerFactory",
transactionManagerRef = "accountTransactionManager",
basePackages = [BASE_PACKAGES],
)
class AccountDatabaseConfig {
}
@EnableJpaRepositories(
entityManagerFactoryRef = "paymentEntityManagerFactory",
transactionManagerRef = "masterTransactionManager",
basePackages = [BASE_PACKAGES],
)
class AccountDatabaseConfig {
}
class AccountService(
private val accountRepository
){
@Transactional("accountTransactionManager")
fun refresh() {
accountRepository.findOne()?.let {
accountEntity.refresh()
}
}
}
repository 는 @EnableJpaRepositories 에 설정된 EntityManagerFactory 로 생성한 EntityManager 를 사용하고, @Transactional 의 transactionManager 옵션을 이용하면 정상적으로 트랜잭션 처리가 됩니다.
반면 아래는 동일한 DataSource 가 아니므로 트랜잭션 처리가 되지 않습니다.
@Transactional("paymentTransactionManager")
fun refresh() {
accountRepository.findOne()?.let {
accountEntity.refresh()
}
}
multi-datasoruce 인 경우 디폴트로 적용할 DataSource, TransactionManager, EntityManagerFactory 에 @Primary 를 주어야 하고, transactionManager 옵션이 없다면 @Primary 가 적용된 TransactionManager 를 사용합니다.
서로 다른 DataSource 가 같은 Repository 를 이용하는 경우
master/slave 를 나누어 주는 경우처럼 DataSource 는 다르지만 같은 Repository 를 사용하는 경우가 있습니다. 특정 비지니스 로직은 CUD 없이 read-only 로만 구성되어 slave db 를 이용할 수도 있는데, @EnableJpaRepositories 에는 하나의 EntityManagerFactory 만 설정할 수 있고 보통은 master 의 EntityManagerFactory 가 설정됩니다.
이런 경우를 위해 사용하는 시점에 원하는 DataSource 를 선택할 수 있게 설정해야 하는데, LazyConnectionDataSourceProxy 를 이용하면 커넥션 획득을 지연시켜 실제 쿼리를 수행할 때 커넥션을 획득할 수 있습니다.
@Bean("routingDataSource")
fun routingDataSource(
@Qualifier("masterDataSource")
masterDataSource: DataSource,
@Qualifier("slaveDataSource")
slaveDataSource: DataSource
): DataSource {
return DynamicRoutingDataSource().apply {
setTargetDataSources(mapOf("master" to masterDataSource, "slave" to slaveDataSource))
setDefaultTargetDataSource(masterDataSource)
}
}
@Primary
@Bean
fun lazyDataSource(routingDataSource: DataSource): DataSource = LazyConnectionDataSourceProxy(routingDataSource)
위에서 설정한 lazyDataSource 를 EntityManagerFactory 를 생성할 때 넣어주면, 기존의 DataSource 를 먼저 획득하여 원하는 커넥션을 얻을 수 없는 방식과 달리 DataSource proxy 을 획득하고 실제 커넥션 획득 시점을 미루어 필요한 커넥션을 얻을 수 있게 됩니다.
참고로, master/slave replication 와 같이 자주 사용되는 기능의 경우 편의를 위해 드라이버가 기본적으로 제공해주고 있을 수도 있습니다. 예를 들어, aws aurora db cluster 는 primary(writer) db 를 사용할 수 없을 때 replica 중 하나를 primary db 로 승격시키는 방법으로 failover 기능을 제공합니다. aws-advanced-jdbc-wrapper 는 mysql-connector-j 같은 기존 드라이버의 기능을 확장하여 failover 기능을 호환해줍니다.
각 DB 벤더는 자신의 DB 에 맞게 JDBC 인터페이스를 구현하고 자신만의 고유한 기능을 추가한 드라이버를 제공하므로, 현재 사용하는 드라이버의 Docs 를 잘 살펴보면 좋을 것 같습니다.
'Spring' 카테고리의 다른 글
Kubernetes 에서 안전하게 힙 덤프 수행하기 (0) | 2024.06.13 |
---|---|
Github Rest API 를 통한 테스트 자동화 (0) | 2024.03.31 |
멀티 스레드 환경에서의 동시성 이슈와 Blocking, Non-blocking 알고리즘을 이용한 동기화 (0) | 2023.12.19 |
자바의 리플렉션(Reflection) (0) | 2023.09.02 |
Speing Webflux 이해하기 (0) | 2023.08.22 |