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 ์ฌ์ฉ๋ฒ๊ณผ ํ
์คํธ ์ฝ๋ ์์ฃผ๋ก ์ค๋ช
ํ๋๋ก ํ๊ฒ ์ต๋๋ค.
๋๊ธ๋จ๊ธฐ๊ธฐ