์—…๋ฐ์ดํŠธ:

๐Ÿ“Œ ๋ชจ๋“  ์†Œ์Šค๋Š” 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() ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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 ์‚ฌ์šฉ๋ฒ•๊ณผ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์œ„์ฃผ๋กœ ์„ค๋ช…ํ•˜๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

๋Œ“๊ธ€๋‚จ๊ธฐ๊ธฐ