๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐Ÿง‘๐Ÿป‍๐Ÿ’ป ๊ฐœ๋ฐœ/Java & Spring

Spring AOP - Master & Slave DB ์ฟผ๋ฆฌ ๋ถ„์‚ฐํ•˜๊ธฐ

by hossii 2024. 2. 5.

DB ์ด์ค‘ํ™”๋ฅผ ๊ตฌ์ถ•ํ–ˆ๋‹ค๋ฉด ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ๋„ ์ฝ๊ธฐ ์ „์šฉ DB๋ฅผ ๊ตฌ๋ถ„ํ•ด์„œ ์ฟผ๋ฆฌ๋ฅผ ๋ถ„์‚ฐํ•ด์•ผ ํ•œ๋‹ค. ๋จผ์ € ์ฟผ๋ฆฌ ๋ถ„์‚ฐ์— ๋Œ€ํ•œ ์ •์ฑ…์„ ์„ค์ •ํ•ด์•ผ ํ•˜๋Š”๋ฐ, ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ˜•ํƒœ๋กœ ์ฟผ๋ฆฌ ๋ถ„์‚ฐ์„ ์‹œ์ผœ๋ณผ ์˜ˆ์ •์ด๋‹ค.

๊ทœ์น™ 1. @RouteDataSource(type)์„ ์ด์šฉํ•ด DB๋ฅผ ๊ฒฐ์ • (type: MASTER | SLAVE)
๊ทœ์น™ 2. @Transactional(readOnly)์„ ์ด์šฉํ•ด DB๋ฅผ ๊ฒฐ์ • (readOnly: true | false)
@RouteDataSource(MASTER)
public service() {
  // ...
}

@Transactional(readOnly=true)
public service() {
  // ...
}

 

Datasource ์ •๋ณด ๋“ฑ๋ก

๋จผ์ € yaml ํŒŒ์ผ ์ž‘์„ฑ์„ ํ•ด์•ผ ํ•˜๋Š”๋ฐ, ํŠน๋ณ„ํ•œ ๊ฒƒ์€ ์—†๊ณ  Master, Slave์— ๋Œ€ํ•œ DataSource ์ •๋ณด๋ฅผ ์ž‘์„ฑํ•ด์ฃผ๋ฉด ๋œ๋‹ค. 

 

`application.yml`

spring:
  datasource:
    master:
      hikari:
        username: ${MASTER_USERNAME}
        password: ${MASTER_PASSWORD}
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: ${MASTER_JDBC_URL}
        
    slave:
      hikari:
        username: ${SLAVE_USERNAME}
        password: ${SLAVE_PASSWORD}
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: ${SLAVE_JDBC_URL}

 

์ฐธ๊ณ ๋กœ jdbc-url์€ `jdbc:mysql://{RDS ์—”๋“œํฌ์ธํŠธ}/{DB ์ด๋ฆ„}` ํ˜•์‹์œผ๋กœ ์ž‘์„ฑํ•˜๋ฉด๋œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๋‚˜์„œ Master DB์™€ ํ†ต์‹ ํ•  Bean ๊ฐ์ฒด์™€ Slave DB์™€ ํ†ต์‹ ํ•  Bean ๊ฐ์ฒด๋ฅผ ๋“ฑ๋กํ•ด์•ผ ํ•œ๋‹ค.

 

`DataSourceConfig.java`

package com.anchor.global.db;

import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateProperties;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateSettings;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@EnableTransactionManagement
public class DataSourceConfig {

  private final String MASTER_DATASOURCE = "masterDataSource";
  private final String SLAVE_DATASOURCE = "slaveDataSource";
  private final String ROUTING_DATASOURCE = "routingDataSource";
  private final String LAZY_ROUTING_DATASOURCE = "lazyRoutingDataSource";
  private final HibernateProperties hibernateProperties;
  private final JpaProperties jpaProperties;

  public DataSourceConfig(JpaProperties jpaProperties) {
    this.jpaProperties = jpaProperties;
    this.hibernateProperties = new HibernateProperties();
    hibernateProperties.setDdlAuto("create-drop");
  }

  /*
   * yml ํŒŒ์ผ์— ์ •์˜ํ•œ Master DB ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์™€ DataSource Bean ๋“ฑ๋ก
   */
  @Bean(MASTER_DATASOURCE)
  @ConfigurationProperties(prefix = "spring.datasource.hikari.master")
  public DataSource masterDataSource() {
    return DataSourceBuilder.create()
        .build();
  }

  /*
   * yml ํŒŒ์ผ์— ์ •์˜ํ•œ Slave DB ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์™€ DataSource Bean ๋“ฑ๋ก
   */
  @Bean(SLAVE_DATASOURCE)
  @ConfigurationProperties(prefix = "spring.datasource.hikari.slave")
  public DataSource slaveDataSource() {
    return DataSourceBuilder.create()
        .build();
  }

  @DependsOn({MASTER_DATASOURCE, SLAVE_DATASOURCE})
  @Bean(ROUTING_DATASOURCE)
  public AbstractRoutingDataSource routingDataSource(
      @Qualifier(MASTER_DATASOURCE) DataSource masterDataSource,
      @Qualifier(SLAVE_DATASOURCE) DataSource slaveDataSource
  ) {
    AbstractRoutingDataSource routingDataSource = new AbstractRoutingDataSource() {
      @Override
      protected Object determineCurrentLookupKey() {
        return RouteDataSourceManager.getCurrentDataSourceName();
      }
    };
    Map<Object, Object> targetDataSources = new HashMap<>();
    targetDataSources.put(RouteDataSource.DataSourceType.MASTER, masterDataSource);
    targetDataSources.put(RouteDataSource.DataSourceType.SLAVE, slaveDataSource);

    routingDataSource.setTargetDataSources(targetDataSources);
    routingDataSource.setDefaultTargetDataSource(masterDataSource);

    return routingDataSource;
  }

  @DependsOn(ROUTING_DATASOURCE)
  @Bean(LAZY_ROUTING_DATASOURCE)
  public LazyConnectionDataSourceProxy lazyRoutingDataSource(
      @Qualifier(ROUTING_DATASOURCE) DataSource routingDataSource) {
    return new LazyConnectionDataSourceProxy(routingDataSource);
  }

  @Bean
  public EntityManagerFactoryBuilder entityManagerFactoryBuilder() {
    return new EntityManagerFactoryBuilder(new HibernateJpaVendorAdapter(), new HashMap<>(), null);
  }

  @DependsOn(LAZY_ROUTING_DATASOURCE)
  @Bean
  public LocalContainerEntityManagerFactoryBean entityManagerFactory(
      EntityManagerFactoryBuilder builder,
      @Qualifier(LAZY_ROUTING_DATASOURCE) DataSource lazyRoutingDataSource) {
    var props = hibernateProperties.determineHibernateProperties(jpaProperties.getProperties(),
        new HibernateSettings());
    return builder
        .dataSource(lazyRoutingDataSource)
        .packages("com.anchor.domain.**.domain")
        .properties(props)
        .persistenceUnit("common")
        .build();
  }

  @DependsOn(LAZY_ROUTING_DATASOURCE)
  @Bean
  public JdbcTemplate jdbcTemplate(@Qualifier(LAZY_ROUTING_DATASOURCE) DataSource dataSource) {
    return new JdbcTemplate(dataSource);
  }

  @Bean
  public PlatformTransactionManager transactionManager(
      EntityManagerFactory entityManagerFactory) {
    return new JpaTransactionManager(entityManagerFactory);
  }

  @Bean
  public JPAQueryFactory JPAQueryFactory(EntityManager entityManager) {
    return new JPAQueryFactory(entityManager);
  }

}

 

์ž‘์„ฑํ•ด์•ผ ํ•˜๋Š” ํด๋ž˜์Šค๋“ค์ด ๋” ์žˆ๋Š”๋ฐ, ํ•ต์‹ฌ์ ์ธ ํด๋ž˜์Šค์— ๋Œ€ํ•œ ์„ค๋ช…๊ณผ ํ•จ๊ป˜ ์•„๋ž˜์— ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ฒ ๋‹ค.

 

๊ด€๋ จ ํด๋ž˜์Šค ๋ถ„์„

1. DataSourceConfig

Spring Data JPA์˜ Auto Configuration์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ๋ณ„๋„์˜ ์„ค์ •์„ ํ•˜๊ธฐ ์œ„ํ•œ ์„ค์ • ํด๋ž˜์Šค์ด๋‹ค.

 

2. AbstractRoutingDataSource

DataSource์— ๋Œ€ํ•œ Routing ์ •์ฑ…์„ ๊ฒฐ์ •ํ•˜๋Š” ์—ญํ• ์„ ํ•˜๋Š” ์ถ”์ƒ ํด๋ž˜์Šค์ด๋‹ค. ๊ฐ€์žฅ ๋จผ์ € determineCurrentLookupKey()๋ฅผ ์ •์˜ํ•ด์•ผ ํ•˜๋Š”๋ฐ Lookup Key๋Š” DataSource๋ฅผ ๊ฒฐ์ •ํ•˜๋Š” ์‹๋ณ„์ž ์—ญํ• ์„ ํ•œ๋‹ค. ์ฆ‰, determineCurrentLookupKey()๋ฅผ ์–ด๋–ป๊ฒŒ ์ •์˜ํ•˜๋А๋ƒ์— ๋”ฐ๋ผ ์‚ฌ์šฉํ•  DB๋ฅผ ์„ ํƒํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ๋‹ฌ๋ผ์ง„๋‹ค.

์ดํ›„ setTaretDataSources()๋ฅผ ํ†ตํ•ด LookupKey์™€ DataSoucre Bean์„ ๋“ฑ๋กํ•ด์ฃผ๋ฉด initialize()๋ฅผ ํ†ตํ•ด resolvedDataSources๋กœ ๋ณต์‚ฌ๋œ๋‹ค. 

JDBC Connection์„ ํ•„์š”๋กœ ํ•  ๋•Œ determineTargetLookupKey()์˜ ๊ฒฐ๊ณผ์— ๋”ฐ๋ผ DataSource๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค.

 

3. RouteDataSourceManager + 

`RouteDataSourceManager`

package com.anchor.global.db;

import static com.anchor.global.db.RouteDataSource.DataSourceType.MASTER;
import static com.anchor.global.db.RouteDataSource.DataSourceType.SLAVE;

import org.springframework.transaction.support.TransactionSynchronizationManager;

public class RouteDataSourceManager {

  private static final ThreadLocal<RouteDataSource.DataSourceType> currentDataSourceName = new ThreadLocal<>();

  public static RouteDataSource.DataSourceType getCurrentDataSourceName() {
    if (currentDataSourceName.get() == null) {
      return TransactionSynchronizationManager.isCurrentTransactionReadOnly() ? SLAVE : MASTER;
    }
    return currentDataSourceName.get();
  }

  public static void setCurrentDataSourceName(RouteDataSource.DataSourceType dataSourceType) {
    currentDataSourceName.set(dataSourceType);
  }

  public static void removeCurrentDataSourceName() {
    currentDataSourceName.remove();
  }

}

Routing ์ •์ฑ…์„ ์‹ค์ œ๋กœ ์ •์˜ํ•œ ์ปค์Šคํ…€ ํด๋ž˜์Šค์ด๋‹ค. DB ํƒ€์ž…์„ ์„ธํŒ…ํ•˜๊ณ  ์ง€์šธ ์ˆ˜ ์žˆ๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ํฌํ•จํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ThreadLocal์„ ํ†ตํ•ด ํ•ด๋‹น ๊ฐ’์„ ์ €์žฅํ•  ์ˆ˜ ์žˆ๋‹ค. ThreadLocal์€ ์Šค๋ ˆ๋“œ์—๊ฒŒ ๋…๋ฆฝ์ ์œผ๋กœ ๊ฐ’์„ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” ํด๋ž˜์Šค์ด๋‹ค. ์š”์ฒญ ์Šค๋ ˆ๋“œ๋ณ„๋กœ ํ˜ธ์ถœ๋œ ๋ฉ”์„œ๋“œ์˜ DataSource ํƒ€์ž…์ด ๋‹ค๋ฅด๊ธฐ ๋•Œ๋ฌธ์— ThreadLocal์„ ํ†ตํ•ด DataSourceType์„ ๋…๋ฆฝ์ ์œผ๋กœ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค. 

 

`RouteDataSource`

package com.anchor.global.db;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RouteDataSource {

  DataSourceType dataSourceType();

  enum DataSourceType {
    MASTER, SLAVE
  }

}

๋‹ค์Œ์œผ๋กœ ์„œ๋น„์Šค ๋ฉ”์„œ๋“œ์˜ DB๋ฅผ ๊ตฌ๋ถ„ํ•˜๊ธฐ ์œ„ํ•ด ์ปค์Šคํ…€ ์–ด๋…ธํ…Œ์ด์…˜๊ณผ DB ํƒ€์ž…์„ ์ •์˜ํ•ด์คฌ๋‹ค.

 

`RouteDataSourceAspect`

package com.anchor.global.db;

import static com.anchor.global.db.RouteDataSource.DataSourceType.MASTER;
import static com.anchor.global.db.RouteDataSource.DataSourceType.SLAVE;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class RouteDataSourceAspect {

  @Before("@annotation(com.anchor.global.db.RouteDataSource) && @annotation(target)")
  public void setDataSource(RouteDataSource target) throws Exception {
    if (target.dataSourceType() == MASTER
        || target.dataSourceType() == SLAVE) {
      RouteDataSourceManager.setCurrentDataSourceName(target.dataSourceType());
    } else {
      throw new Exception("Wrong DataSource Type : Should Check Exception");
    }
  }

  @After("@annotation(com.anchor.global.db.RouteDataSource)")
  public void clearDataSource() {
    RouteDataSourceManager.removeCurrentDataSourceName();
  }

}

๊ทธ๋ฆฌ๊ณ  ๋™์ ์œผ๋กœ ์„œ๋น„์Šค ๋ฉ”์„œ๋“œ ์œ„์— ๋ถ™์€ DB ํƒ€์ž…์„ ํฌํ•จํ•˜๋Š” ์–ด๋…ธํ…Œ์ด์…˜์„ ํš๋“ํ•˜๊ณ , ์ด๋ฅผ RouteDataSourceManager์— ๋“ฑ๋กํ•  ์ˆ˜ ์žˆ๋„๋ก Spring AOP๋ฅผ ํ™œ์šฉํ•œ ํด๋ž˜์Šค๋ฅผ ์ •์˜ํ–ˆ๋‹ค.

 

4. LazyConnectionDataSourceProxy

LazyConnectionDataSourceProxy๋Š” ์ด๋ฆ„ ๊ทธ๋Œ€๋กœ DataSource์— ๋Œ€ํ•œ Connection์„ ๋‚˜์ค‘์— ๊ฐ€์ ธ์˜ค๋„๋ก ํ•˜๋Š” ํ”„๋ก์‹œ ํด๋ž˜์Šค์ด๋‹ค. Connection ํš๋“์„ ๋ฏธ๋ค„์•ผ ๋˜๋Š” ์ด์œ ๋Š” LazyConnectionDataSourceProxy์—†์ด ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•จ์œผ๋กœ์จ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

 

์ ์šฉ ์ „

์•„๋ž˜๋Š” ๋””๋ฒ„๊น…์„ ํ†ตํ•ด CallStack์— ํ˜ธ์ถœ๋œ ๋ฉ”์„œ๋“œ ์ˆœ์„œ์ด๋‹ค.

1. Transaction์„ ์‹œ์ž‘ํ•˜๊ณ  `DatasourceConnectionProviderImpl.getConnection()`์„ ํ˜ธ์ถœํ•œ๋‹ค.
2. Spring CGLIB์— ์˜ํ•ด ์ƒ์„ฑ๋œ DataSourceConfig ํ”„๋ก์‹œ ๊ฐ์ฒด๋ฅผ ํ†ตํ•ด Connection ํš๋“์„ ์‹œ๋„ํ•œ๋‹ค.
3. ์ดํ›„ `AbstractRoutingSource.determineTargetDataSource()`๋ฅผ ํ†ตํ•ด DataSource๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.
4. ๋‚ด๋ถ€์ ์œผ๋กœ `AbstractRoutingSource.determineCurrentLookupKey()`์™€ RouteDataSourceManager ๋‚ด์— ์žˆ๋Š”  `TransactionSynchronizationManager.isCurrentTransactionReadOnly()`๊ฐ€ ์ˆœ์„œ๋Œ€๋กœ ํ˜ธ์ถœ๋œ๋‹ค.
5. `TransactionSynchronizationManager.setCurrentTransactionReadOnly()`์ด ํ˜ธ์ถœ๋˜๋ฉด์„œ Transaction์˜ ReadOnly ํŠน์„ฑ์„ ์„ธํŒ…ํ•œ๋‹ค.

 

์ด ์ƒํƒœ๋ผ๋ฉด readOnly๊ฐ€ true์ด๋“  false์ด๋“  ํ•ญ์ƒ `TransactionSynchronizationManager.isCurrentTransactionReadOnly()`์˜ ๊ฒฐ๊ณผ๋Š” false๊ฐ€ ๋‚˜์˜ฌ ๊ฒƒ์ด๋‹ค. ์˜๋„ํ•œ๋Œ€๋กœ ๋™์ž‘ํ•˜๋ ค๋ฉด `TransactionSynchronizationManager.setCurrentTransactionReadOnly()`๊ฐ€ `AbstractRoutingSource.determineDataSource()`๋ณด๋‹ค ๋จผ์ € ํ˜ธ์ถœ๋ผ์•ผ ํ•œ๋‹ค.

 

์ด๋Ÿฌํ•œ ์ด์œ ๋กœ AbstractRoutingDataSource๋ฅผ LazyConnectionDataSourceProxy๋กœ ๊ฐ์‹ธ์ฃผ๊ฒŒ ๋˜๋ฉด Connection Proxy๋ฅผ ์šฐ์„  ๊ฐ€์ ธ์˜ค๊ณ , Connection์„ ์ง์ ‘ ์‚ฌ์šฉํ•  ๋•Œ๊ฐ€ ๋˜์„œ์•ผ DataSource๋กœ๋ถ€ํ„ฐ Connection์„ ๊ฐ€์ ธ์˜จ๋‹ค. ๊ฒฐ๊ณผ์ ์œผ๋กœ DataSourceConfig ํ”„๋ก์‹œ ๊ฐ์ฒด๋ฅผ ํ†ตํ•ด Connection ํš๋“์„ ๋’ค๋กœ ๋ฏธ๋ค„ Transaction์˜ ReadOnly ํŠน์„ฑ์„ ๋จผ์ € ์„ธํŒ…ํ•ด์ค„ ์ˆ˜ ์žˆ๋‹ค.

 

์ ์šฉ ํ›„

๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ˆœ์„œ๊ฐ€ ๋ณ€๊ฒฝ๋œ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

1. Transaction์„ ์‹œ์ž‘ํ•˜๊ณ  `DatasourceConnectionProviderImpl.getConnection()`์„ ํ˜ธ์ถœํ•œ๋‹ค.
2.`TransactionSynchronizationManager.setCurrentTransactionReadOnly()`์ด ํ˜ธ์ถœ๋˜๋ฉด์„œ Transaction์˜ ReadOnly ํŠน์„ฑ์„ ์„ธํŒ…ํ•œ๋‹ค.
3. Spring CGLIB์— ์˜ํ•ด ์ƒ์„ฑ๋œ DataSourceConfig ํ”„๋ก์‹œ ๊ฐ์ฒด๋ฅผ ํ†ตํ•ด Connection ํš๋“์„ ์‹œ๋„ํ•œ๋‹ค.
4. ์ดํ›„ `AbstractRoutingSource.determineTargetDataSource()`๋ฅผ ํ†ตํ•ด DataSource๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.
5. ๋‚ด๋ถ€์ ์œผ๋กœ `AbstractRoutingSource.determineCurrentLookupKey()`์™€ RouteDataSourceManager ๋‚ด์— ์žˆ๋Š”  `TransactionSynchronizationManager.isCurrentTransactionReadOnly()`๊ฐ€ ์ˆœ์„œ๋Œ€๋กœ ํ˜ธ์ถœ๋œ๋‹ค.

๋ฐœ์ƒํ•œ ์ด์Šˆ

์ด์Šˆ1. p6spy ํ˜ธํ™˜์„ฑ ๋ฌธ์ œ

์›์ธ ๋ถ„์„

์šฐ๋ฆฌ๋Š” ๊ฐœ๋ฐœ๊ณผ์ •์—์„œ p6spy-spring-boot-starter๋ฅผ ์ด์šฉํ•ด ์ฟผ๋ฆฌ ๋กœ๊ทธ๋ฅผ Console์— ์ถœ๋ ฅํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์ฟผ๋ฆฌ๋ฅผ ํ™•์ธํ–ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  DB ์ด์ค‘ํ™” ์„ค์ •์„ ์ ์šฉํ•˜๊ณ  ๋‚œ ๋’ค ๋™์ผํ•œ ์ฟผ๋ฆฌ ๋กœ๊ทธ๊ฐ€ 3๋ฒˆ ์ฐํžˆ๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค.

 

๋””๋ฒ„๊น… ๊ฒฐ๊ณผ, `P6DataSource.getConnection()` ๋ฉ”์„œ๋“œ๊ฐ€ 3๋ฒˆ ํ˜ธ์ถœ๋˜๋ฉด์„œ 3๊ฐœ์˜ ์„œ๋กœ ๋‹ค๋ฅธ 3๊ฐœ์˜ DataSource๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

 

์›์ธ์„ ํŒŒ์•…ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š”  P6Spy์˜ ๋™์ž‘๊ณผ์ •์„ ์ดํ•ดํ•  ํ•„์š”๊ฐ€ ์žˆ๋‹ค. P6Spy๋Š” ๊ตฌ์กฐ์ ์œผ๋กœ JDBC ๋“œ๋ผ์ด๋ฒ„ ์•ž์— P6Spy ๋“œ๋ผ์ด๋ฒ„๋ฅผ ์œ„์น˜์‹œ์ผœ ๋ฐœ์ƒํ•˜๋Š” ์ฟผ๋ฆฌ๋ฅผ ๊ฐ์ง€ํ•˜๊ณ  ๋กœ๊ทธ๋ฅผ ๋‚จ๊ธด๋‹ค.

 

์ข€ ๋” ์ƒ์„ธํ•œ ๋™์ž‘๊ณผ์ •์„ ์‚ดํŽด๋ณด๊ธฐ ์œ„ํ•ด P6DataSource ํด๋ž˜์Šค์˜ ์ƒ์„ฑ์ž์— Break Point๋ฅผ ์ฐ๊ณ  ๋””๋ฒ„๊น…์„ ๋Œ๋ ค๋ณด์•˜๋‹ค.

๋จผ์ € ๋“ฑ๋ก๋œ Bean์„ ์ดˆ๊ธฐํ™”ํ•˜๊ณ  `DataSourceDecoratorBeanPostProcessor.class`์— ์˜ํ•ด PostProcessing์„ ์ง„ํ–‰ํ•œ๋‹ค.

ํ˜ธ์ถœ๋œ `postProcess...` ๋ฉ”์„œ๋“œ๋ฅผ ๋ณด๋ฉด `P6SpyDataSourceDecorator.decorate()` ๋ฉ”์„œ๋“œ์— ์˜ํ•ด ๋“ฑ๋ก๋œ Bean์ด P6DataSource๋กœ ๋ฐ˜ํ™˜๋œ๋‹ค. ์•„๋ž˜ ์‚ฌ์ง„๊ณผ ๊ฐ™์ด ๋ชจ๋“  DataSource Bean๋“ค์ด ์œ„ ๊ณผ์ •์„ ๊ฑฐ์น˜๊ธฐ ๋•Œ๋ฌธ์— ์ด 4๊ฐœ์˜ P6DataSource๊ฐ€ ์ƒ์„ฑ๋œ๋‹ค. ๋ฌธ์ œ ์ƒํ™ฉ์€ ์ด๋Ÿฌํ•œ ์ด์œ ๋กœ ๋ฐœ์ƒํ•œ๋‹ค.

์กฐ๊ธˆ๋งŒ ๋” ์ถ”๊ฐ€ํ•˜์ž๋ฉด, DataSource Bean๋“ค์€ ์ƒ์„ฑ๋œ P6DataSource ๋‚ด์— realDataSource๋กœ ๋“ฑ๋ก๋œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  P6Proxy ๊ตฌํ˜„์ฒด(ConnectionWrapper, StatementWrapper, ...)๋“ฑ์— ์˜ํ•ด wrapping๋จ์œผ๋กœ์จ ์ฟผ๋ฆฌ๋ฅผ ๊ฐ์ง€ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜๋Š” ๊ฒƒ์ด๋‹ค.

 

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

ํŠน์ • DataSource์— ๋Œ€ํ•ด์„œ๋งŒ Decorater๊ฐ€ ์ ์šฉ๋˜๋„๋ก CustomBeanPostProcessor๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋“ฑ์˜ ์—ฌ๋Ÿฌ ์‹œ๋„๋ฅผ ํ•ด๋ดค์ง€๋งŒ ์‹คํŒจํ–ˆ๋‹ค. p6spy-spring-boot-starter ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•˜๋ฉด `DataSourceDecoratorBeanPostProcessor`ํด๋ž˜์Šค๋ฅผ AutoConfiguration์œผ๋กœ ๋“ฑ๋กํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋”ฑํžˆ ๋ฐฉ๋ฒ•์ด ์—†๋Š”๋“ฏ ํ–ˆ๋‹ค.

 

๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ค‘๋ณต ๋กœ๊ทธ๊ฐ€ ์ถœ๋ ฅ๋˜๋Š” ๋ฌธ์ œ ์™ธ์—๋„ LazyConnectionDataSourceProxy๊ฐ€ ์˜๋„๋Œ€๋กœ ์ ์šฉ๋˜์ง€ ์•Š๋Š” ๋ฌธ์ œ๋„ ์žˆ์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ์ด๋ฅผ ํ•ด๊ฒฐํ•˜์ง€ ๋ชปํ•œ๋‹ค๋ฉด ์‚ฌ์šฉ์„ ํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์ด ์˜ณ๋‹ค๊ณ  ํŒ๋‹จํ–ˆ๋‹ค. 

 

์ž๋™ ๊ตฌ์„ฑ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” p6spy ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•ด์„œ ํ•  ์ˆ˜ ์žˆ์„๊ฒƒ ๊ฐ™์ง€๋งŒ, ๊ผญ p6spy๊ฐ€ ์•„๋‹ˆ๋”๋ผ๋„ Hibernate๋ฅผ ํ†ตํ•ด์„œ ๋ณ„๋„์˜ ๋กœ๊น… ์ฒ˜๋ฆฌ๋ฅผ ํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ผ๋‹จ์€ p6spy ์˜์กด์„ฑ์„ ์ œ๊ฑฐํ•˜๋Š” ๋ฐฉํ–ฅ์œผ๋กœ ๊ฒฐ์ •ํ–ˆ๋‹ค.

 

์ด์Šˆ2. ddl-auto ๋ฏธ๋™์ž‘

Master - Slave ๋ฐฉ์‹์œผ๋กœ Bean์„ ๋“ฑ๋กํ•˜๋ฉด `spring.jpa.hibernate.ddl-auto`๋ฅผ ์ ์šฉํ•ด๋„ ์Šคํ‚ค๋งˆ๊ฐ€ ์ƒ์„ฑ๋˜์ง€ ์•Š๋Š”๋‹ค. ๊ทธ ์ด์œ ๋Š” `spring.jpa.hibernate` ์†์„ฑ์€ JpaBaseConfiguration์„ ์ƒ์†ํ•œ HibernateJpaConfiguration ํด๋ž˜์Šค์— ์˜ํ•ด ์ ์šฉ๋˜๋Š”๋ฐ, LocalContainerEntityManagerFactoryBean์„ ์ง์ ‘ ๋“ฑ๋กํ•ด์„œ ํ•ด๋‹น Bean์— ๋Œ€ํ•œ auto configuration์ด ๋™์ž‘ํ•˜์ง€ ์•Š๋Š”๋‹ค.

 

๋”ฐ๋ผ์„œ LocalContainerEntityManagerFactoryBean์„ ์ƒ์„ฑํ•  ๋•Œ HibernateProperties ํด๋ž˜์Šค์˜ determineHibernateProperties() ๋ฉ”์„œ๋“œ๋กœ properties๋ฅผ ๊ตฌ์„ฑํ•ด์„œ ์ฃผ์ž…ํ•ด์•ผ ํ•œ๋‹ค. ์ด๋ฏธ ์œ„์˜ ์ฝ”๋“œ์— ์ž‘์„ฑ๋ผ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์ง„ ์•Š๊ฒ ๋‹ค.

 

๋งˆ๋ฌด๋ฆฌ

p6spy๋ฅผ ์ง€์›€์œผ๋กœ ์ธํ•ด ํ…Œ์ŠคํŠธ๋Š” Master DB์—๋Š” ์—†๊ณ , Slave DB์—๋งŒ ์žˆ๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์ง„ํ–‰ํ–ˆ๋‹ค. ์•ž์„œ ์˜๋„ํ•œ ๊ทœ์น™๋Œ€๋กœ ์ž˜ ๋™์ž‘ํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค. ์ƒ๊ฐํ–ˆ๋˜ ๊ฒƒ๋ณด๋‹ค ๋งŽ์€ ๋‚ด์šฉ๋“ค์ด ๋’ค์„ž์—ฌ ์žˆ์–ด์„œ ์‹œ๊ฐ„์ด ๊ฝค ๊ฑธ๋ ธ๋Š”๋ฐ ์ข‹์€ ๊ฒฝํ—˜์ด ๋˜์—ˆ๋˜ ๊ฒƒ ๊ฐ™๋‹ค.