Spring JPA::Master/Slave ๋ถ๊ธฐ์ฒ๋ฆฌ(1)
์ ๋ฐ์ดํธ:
๐ ๋ชจ๋ ์์ค๋ Github์ ์์ต๋๋ค.
DB Replication
๋จผ์ , Spring์ ์์ ์ ์์๋ณด๊ธฐ ์ ์ DB Replication์ ๋ํด ์ค๋ช ํด๋ณด๊ฒ ์ต๋๋ค.
๋ณดํต ์๋น์ค ๊ธฐ์ ์์ DB์ ๋ํด ๋ง์ Connection๊ณผ Transaction์ด ์ผ์ด๋๊ฒ ๋๊ณ ,
์ด๊ฒ์ ๋ถ์ฐ ์ฒ๋ฆฌ ํ๊ธฐ ์ํด 1๋์ ์ฐ๊ธฐ/์ฝ๊ธฐ ์ฉ๋์ Master DB, ์ฌ๋ฌ๋์ ์ฝ๊ธฐ ์ ์ฉ์ Slave๋ฅผ ๋๊ฒ ๋ฉ๋๋ค.
์ด๋ฅผ DB Replication์ด๋ผ ํฉ๋๋ค.
@Transacional
Spring Framework์์ ์ ๊ณตํ๋ @Transactional
์ด๋
ธํ
์ด์
์ readOnly
์์ฑ์ ํตํ์ฌ ํน์ ๋น์ฆ๋์ค ๋ก์ง์ Master/Slave DB๋ก ๋ถ๊ธฐ์ฒ๋ฆฌ ํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
ํ๊ฒฝ
- Java 11
- spring boot 2.5.7
- gradle 7.2
- h2
Dependencies
๐๏ธ build.gradle
dependencies{
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
์คํ๋ง ์ค์
๐๏ธ application-test.yml
spring:
application:
name: jpa-multidb-connection
config:
activate:
on-profile:
- test
jpa:
hibernate:
ddl-auto: create-drop
generate-ddl: true
properties:
hibernate:
format_sql: true
use_sql_comments: true
datasource:
master:
driver-class-name: org.h2.Driver
jdbc-url: jdbc:h2:tcp://localhost/~/master
read-only: false
username: sa
password:
slave:
driver-class-name: org.h2.Driver
jdbc-url: jdbc:h2:tcp://localhost/~/slave
read-only: true
username: sa
password:
logging:
level:
org.hibernate.SQL: debug
org.hibernate.type: trace
com.citizen.multidb: debug
์ฝ๋
@SpringBootApplication
์ฐ์ ์ ๊ฐ ๋ง๋ค DataSource
ํด๋์ค๊ฐ ์ ์ฉ๋์ด์ผ ํ๊ธฐ ๋๋ฌธ์ Spring Boot์ Auto-Configuration ํด๋์ค ์ค ํ๋์ธ DataSourceAutoConfiguration
์ ์ ์ธ ์ํต๋๋ค.
@AutoConfigureBefore
๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ๋ ์์ผ๋ ์ด๋ฒ์๋ ์์ ์ ์ธ์์ผฐ์ต๋๋ค.
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class MultidbApplication {
public static void main(String[] args) {
SpringApplication.run(MultidbApplication.class, args);
}
}
ReplicationRouingDataSource
@Transactional
์ readOnly
์ ๋ฐ๋ผ ์ ๊ทผํ DB, Master์ Slave๋ฅผ ๊ตฌ๋ถ์ง๋๋ก ํ๊ฒ ์ต๋๋ค.
@Slf4j
public class ReplicationRoutingDataSource extends AbstractRoutingDataSource // -(1) {
@Override
protected Object determineCurrentLookupKey() {
if (log.isDebugEnabled()) {
log.debug("current getCurrentTransactionName : {}", TransactionSynchronizationManager.getCurrentTransactionName());
log.debug("current isCurrentTransactionReadOnly : {}", TransactionSynchronizationManager.isCurrentTransactionReadOnly());
log.debug("current isActualTransactionActive : {}", TransactionSynchronizationManager.isActualTransactionActive());
log.debug("current getCurrentTransactionIsolationLevel : {}", TransactionSynchronizationManager.getCurrentTransactionIsolationLevel());
log.debug("current getResourceMap : {}", TransactionSynchronizationManager.getResourceMap());
}
return TransactionSynchronizationManager.isCurrentTransactionReadOnly() ? "slave" : "master"; // -(2)
}
}
(1) AbstractRoutingDataSource
org.springframework.jdbc.datasource.lookup
ํจํค์ง์์ ์ ๊ณตํ๋ ์ถ์ ํด๋์ค๋ก ์ฌ๋ฌ๊ฐ์ง DataSource๋ฅผ Map์ผ๋ก ๋ด๊ณ Customํ๊ฒ ์ค์ ํ ๋ก์ง์ ๋ฐ๋ผ ๋ถ์ DB๋ฅผ ๊ฒฐ์ ํ๊ฒ ํฉ๋๋ค.
determineCurrentLookupKey()
๋ฉ์๋๋ฅผ Overrideํ์ฌ TargetDataSource์ ํค๋ฅผ ์ด๋ค์์ผ๋ก ๊ฒฐ์ ํ ์ง ์ฝ๋๋ฅผ ์์ฑํฉ๋๋ค.
์ ๋ readOnly์ ๋ฐ๋ผ master์ slave๋ฅผ ๊ฒฐ์ ํ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ ์๋์ ๊ฐ์ด ์ฝ๋๋ฅผ ์งฐ์ต๋๋ค.
(2) TransactionSynchronizationManager
org.springframework.transaction.support
์ TransactionSynchronizationManager
์ isCurrentTransactionReadOnly()
๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ฉด ํ์ฌ Transaction์ readOnly ์์ฑ์ ๊ฐ์ ธ์ต๋๋ค.
๋ฐ๋ผ์, readOnly์ผ ๊ฒฝ์ฐ Slave๋ฅผ readOnly๊ฐ ์๋ ๊ฒฝ์ฐ Master์ ๊ตฌ๋ถ์๋ฅผ ๋ฐํํ๋๋ก ํฉ๋๋ค.
๋ฒ์ธ
๋ฒ์ธ๋ก AbstractRoutingDataSource
์์ ์ด๋ค ์์ ์ DataSource๊ฐ ๊ฒฐ์ ๋๋์ง ๊ถ๊ธํ์ฌ ๋ด๋ถ๋ฅผ ์ดํด๋ณด์์ต๋๋ค.
๋ด๋ถ์์ determineTargetDataSource()
๋ผ๋ ๋ฉ์๋์ getConnection()
ํ์ธํ ์ ์์ต๋๋ค.
AbstractRoutingDataSource
๋ฅผ ์์ํ ReplicationRoutingDataSource
๊ฐ Spring Bean์ผ๋ก ๋ฑ๋ก๋๊ณ , getConnection()
์ด ํธ์ถ๋ ๋, determineCurrentLookupKey()
โ determineTargetDateSource()
๋ฅผ ํธ์ถํ์ฌ DataSource๋ฅผ ๊ฒฐ์ ํ๋ ๊ฒ์ด์์ต๋๋ค.
๋ํ determineTargetDateSource()
๊ฐ protected
๋ก ๋์ด์๋ ๊ฒ์ ๋ณด์ํ๋ ์ด ๋ฉ์๋๋ Overrideํ์ฌ ์ฌ์ ์๋ก ์ฌ์ฉํ ์ ์๊ฒ ๋ค ์ถ์์ต๋๋ค.
DbConfig
๋ค์์ผ๋ก ์ค์ ๋ก ReplicationRoutingDataSource
๋ฅผ ๊ฐ์ง๊ณ Spring์ ์ ์ฉ๋ Config ํด๋์ค๋ฅผ ๋ง๋ค์ด ๋ณด๊ฒ ์ต๋๋ค.
DbConfig
ํด๋์ค์๋ Master/Slave, ReplicationRouting DataSource, EntityManagerFactory, TransactionManager๋ฅผ Bean์ผ๋ก ์ค์ ํฉ๋๋ค.
@Configuration
@EnableJpaRepositories( // -(1)
entityManagerFactoryRef = DbConfig.MULTI_DB_ENTITY_MANAGER,
transactionManagerRef = DbConfig.MULTI_DB_TX_MANAGER,
basePackages = DbConfig.MULTI_DB_COMPONENT_PACKAGE
)
public class DbConfig {
public static final StringMULTI_DB_ENTITY_MANAGER= "multidbEntityManager";
public static final StringMULTI_DB_TX_MANAGER= "multidbTransactionManager";
public static final StringMULTI_DB_COMPONENT_PACKAGE= "com.citizen.multidb.domain";
private final String MULTI_DB_MASTER_DATA_SOURCE = "multidbMasterDataSource";
private final String MULTI_DB_SLAVE_DATA_SOURCE = "multidbSlaveDataSource";
private final String MULTI_DB_ROUTING_DATA_SOURCE = "multidbRoutingDataSource";
private final String MULTI_DB_DATA_SOURCE = "multidbDataSource";
private final String MULTI_DB_MASTER_PROPERTIES_PREFIX = "spring.datasource.master";
private final String MULTI_DB_SLAVE_PROPERTIES_PREFIX = "spring.datasource.slave";
private final String MULTI_DB_PERSISTENCE_UNIT = "multidb";
// -(2)
@ConfigurationProperties(prefix = MULTI_DB_MASTER_PROPERTIES_PREFIX)
@Bean(name = MULTI_DB_MASTER_DATA_SOURCE)
public DataSource multidbMasterDataSource() {
return new HikariDataSource();
}
// -(3)
@ConfigurationProperties(prefix = MULTI_DB_SLAVE_PROPERTIES_PREFIX)
@Bean(name = MULTI_DB_SLAVE_DATA_SOURCE)
public DataSource multidbSlaveDataSource() {
return new HikariDataSource();
}
// -(4)
@Bean(name = MULTI_DB_ROUTING_DATA_SOURCE)
public DataSource multidbRoutingDataSource(
@Qualifier(MULTI_DB_MASTER_DATA_SOURCE) DataSource multiMasterDataSource,
@Qualifier(MULTI_DB_SLAVE_DATA_SOURCE) DataSource multiSlaveDataSource
) {
ReplicationRoutingDataSource replicationRoutingDataSource = new ReplicationRoutingDataSource();
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put("master", multiMasterDataSource);
dataSourceMap.put("slave", multiSlaveDataSource);
replicationRoutingDataSource.setTargetDataSources(dataSourceMap);
replicationRoutingDataSource.setDefaultTargetDataSource(multiMasterDataSource);
return replicationRoutingDataSource;
}
// -(5)
@Primary
@DependsOn({MULTI_DB_MASTER_DATA_SOURCE, MULTI_DB_SLAVE_DATA_SOURCE,
MULTI_DB_ROUTING_DATA_SOURCE})
@Bean(name = MULTI_DB_DATA_SOURCE)
public DataSource multidbDataSource(
@Qualifier(MULTI_DB_ROUTING_DATA_SOURCE) DataSource multidbRoutingDataSource) {
return new LazyConnectionDataSourceProxy(multidbRoutingDataSource);
}
// -(6)
@Primary
@Bean(name =MULTI_DB_ENTITY_MANAGER)
public LocalContainerEntityManagerFactoryBean multidbEntityManager(
@Qualifier(MULTI_DB_DATA_SOURCE) DataSource multidbDataSource,
EntityManagerFactoryBuilder builder
) {
return builder
.dataSource(multidbDataSource)
.packages(MULTI_DB_COMPONENT_PACKAGE)
.persistenceUnit(MULTI_DB_PERSISTENCE_UNIT)
.build();
}
// -(7)
@Primary
@Bean(name =MULTI_DB_TX_MANAGER)
public PlatformTransactionManager multidbTransactionManager(
@Qualifier(MULTI_DB_ENTITY_MANAGER) EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
}
(1) @EnableJpaRepositories
@EnableJpaRepositories
๋ฅผ ์ฌ์ฉํ์ฌ Repository๊ฐ ์๋ package, EntityManagerFactory, TransactionManager๋ฅผ ์ง์ ํด ์ค๋๋ค.
(2) multidbMasterDataSource
@ConfigurationProperties
๋ฅผ ์ฌ์ฉํ์ฌ application-test.yml
์ ์์ฑํ spring.datasource.master
๊ฐ์ ์ฝ์ด HikariDataSource()
์ binding ํ๊ณ , ํด๋น ๋ฉ์๋๋ฅผ Bean์ผ๋ก ๋ฑ๋กํฉ๋๋ค.
(3) multidbSlaveDataSource
multidbMasterDataSource์ ๊ฐ์ ๋ฐฉ์์ผ๋ก slave์ ์ ๋ณด๋ฅผ ํ ๋๋ก Bean์ผ๋ก ๋ฑ๋กํฉ๋๋ค.
(4) multidbRoutingDataSource
์์ฑํ ReplicationRoutingDataSource
์ Master/Slave์ ๋ํ DataSource์ key๋ฅผ ๋ฃ์ Map์ TargetDataSource๋ก ์ค์ ํ๊ณ , Default๋ฅผ Master๋ฅผ ๋ณด๋๋ก DefaultTargetDataSource๋ก ๋ฑ๋กํฉ๋๋ค.
(5) multidbDataSource
์์ multidbRoutingDataSource๋ฅผ ๊ธฐ๋ฐ์ผ๋ก LazyConnectionDataSourceProxy
๋ฅผ ์์ฑํ์ฌ ๋ฐํํ๊ณ ์ด๋ฅผ @Primary
์ฆ, DataSource ์ค์์ ์ฐ์ ํ์ฌ ์ฌ์ฉํ๋๋ก ์ง์ ํฉ๋๋ค.
๐กSpring์ ํธ๋์ญ์ ์ ์ง์ ํ๋ ์๊ฐ DataSource์ Connection์ ๊ฐ์ ธ์ต๋๋ค. (์ฌ์ง์ด ์๋ฌด ์ฌ์ฉ๋ ํ์ง ์๋ ๊ฒฝ์ฐ์๋) ์ง๊ธ ์ ๊ฐ ์ค์ตํ๋ Master/Slave ํ๊ฒฝ์์ ํธ๋์ญ์ ์ง์ ์์ ์์ ์ปค๋ฅ์ ์ ๊ฒฐ์ ํ๊ฒ ๋๋ฉด readOnly์ ์์ฑ๊ณผ๋ ์๊ด์์ด Master๋ก๋ง Connection์ด ๋ถ๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ๊ฒ์ ๋๋ค. ๋ฐ๋ผ์,
LazyConnectionDataSourceProxy
๋ฅผ ์ฌ์ฉํ์ฌ ์ค์ ๋ก ์ปค๋ฅ์ ์ด ํ์ํ ์ํฉ์์๋ง ์ปค๋ฅ์ ์ ์ฌ์ฉํ๋๋ก ํฉ๋๋ค.
(6) multidbEntityManager
LocalContainerEntityManagerFactoryBean
์ ๋ฐํํ๋ EntityManagerFactory์ ์์์ Bean์ผ๋ก ๋ฑ๋กํ multidbDataSource, Entity ํจํค์ง ๊ฒฝ๋ก, persistenceUnit์ ์ธํ
ํ์ฌ ๋ฐํํ๋ค.
โ์ฌ์ค EntityManagerFactory๋ฅผ Bean์ผ๋ก ๋ฑ๋กํ๋๊ฑด ์๊ฒ ๋๋ฐ ์
LocalContainerEntityManagerFactoryBean
๊ฐ ์ฌ์ฉ๋๋์ง ๋๋ ์ ๋ ์ ๋ชจ๋ฅด๊ฒ ์ต๋๋ค;; ์ถํ ๊ณต๋ถํด์ ํฌ์คํธ์ ์ถ๊ฐํ๋๋ก ํ๊ฒ ์ต๋๋ค~
(7) multidbTransactionManager
TransactionManager๋ก์จ ์ ๋ Jpa๋ฅผ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ JpaTransactionManager
๋ฅผ ๋ฐํํด์ค๋๋ค.
โSpring์์ ๋ก์ฐ๋ ๋ฒจ์ ํธ๋์ญ์ ์๋น์ค๋ฅผ ์ด์ฉํ๊ธฐ ์ํด
PlatformTransactionManager
๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ ์๊ฒ ์ง๋ง, ์ ํํ ์ด๋ค ๋ฐฉ์์ผ๋ก ๋์ํ๋์ง ์์ง ํ์ต์ด ๋ถ์กฑํ๋ค์.. ์์LocalContainerEntityManagerFactoryBean
์ ๋ง์ฐฌ๊ฐ์ง๋ก ํ์ตํ๋๋๋ก ํฌ์คํธ์ ๋ด์ฉ์ ์ถ๊ฐํ๋๋ก ํ๊ฒ ์ต๋๋ค.
๋ง๋ฌด๋ฆฌ
๊ธ์ด ๋๋ฌด ๊ธธ์ด์ง๋๊ฑฐ ๊ฐ์ ์ด๋ฒ ํฌ์คํ
์ ์ค์ ๊ด๋ จ ํด๋์ค ์์ฑ๋ฒ์ผ๋ก ๋ง๋ฌด๋ฆฌํ๊ณ ๋ค์์ ๋น์ฆ๋์ค ์ฝ๋์์ @Transactional
์ readOnly
์ฌ์ฉ๋ฒ๊ณผ ํ
์คํธ ์ฝ๋ ์์ฃผ๋ก ์ค๋ช
ํ๋๋ก ํ๊ฒ ์ต๋๋ค.
๋๊ธ๋จ๊ธฐ๊ธฐ