2022.03.23 - [데이터베이스 노트/데이터베이스] - Database의 레플리케이션(Replication) (Master/Slave DB)
앞에서 Master/Slave DB 환경을 구축하였을 때의 이점을 살펴보았으니 애플리케이션단에서 구현하는 방법을 살펴보자.
- 개발환경 : MyBatis
일단 각 DB 별 DataSource가 필요하므로 DataSoruce를 정의해 준다.
...
// Master DataSource
@Bean(name = MASTER_DATASOURCE)
public HikariDataSource masterDataSource() throws SQLException {
return new HikariDataSource(getHikariConfig(getOracleDataSource(), MASTER_DATASOURCE.concat("-pool")));
}
// Slave DataSource
@Bean(name = SLAVE_DATASOURCE)
public HikariDataSource slaveDataSource() throws SQLException {
return new HikariDataSource(getHikariConfig(getOracleDataSource(), SLAVE_DATASOURCE.concat("-pool")));
}
...
1. Multi DataSource 구현
처음 생각한 방법은 Master/Slave 별로 SqlSessionFactory를 만들어 이용하는 것 이었다.
@Configuration
public class SimpleSqlSessionFactory {
// MasterDataSource를 이용하여 생성
@Bean(name = MASTER_SQL_SESSION_FACTORY)
public SqlSessionFactoryBean masterSqlSessionFactoryBean(@Qualifier(MASTER_DATASOURCE) DataSource dataSource,
ApplicationContext applicationContext) throws IOException {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setConfigLocation(applicationContext.getResource("classpath:common/mybatis/config.xml"));
factoryBean.setMapperLocations(applicationContext.getResources("classpath*:common/mybatis/mappers/master/**/*.xml"));
factoryBean.setTypeHandlersPackage("com.example.sample.config.mybatis");
return factoryBean;
}
// SlaveDataSource를 이용하여 생성
@Bean(name = SLAVE_SQL_SESSION_FACTORY)
public SqlSessionFactoryBean slaveSqlSessionFactoryBean(@Qualifier(SLAVE_DATASOURCE) DataSource dataSource,
ApplicationContext applicationContext) throws IOException {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setConfigLocation(applicationContext.getResource("classpath:common/mybatis/config.xml"));
factoryBean.setMapperLocations(applicationContext.getResources("classpath*:common/mybatis/mappers/slave/**/*.xml"));
factoryBean.setTypeHandlersPackage("com.example.sample.config.mybatis");
return factoryBean;
}
}
Mapper의 경로 설정을 달리하여, 쿼리들을 나눠(Master/Slave) 관리/사용하는 방법이다.
가장 단순한 방법이지만 Mapper파일을 목적에 따라 따로 관리해야하는 점과 트랜잭션내에서 주의해야하는 점 등등 몇가지 단점들이 있다.
처음 해당 환경을 구축할 때 이 방법을 아예 제외하였는데, 가중 큰 이유는 사용 중이던 Select 쿼리들을 별도의 파일(xxxMapper.xml)로 분리하는 작업이 필요했기 때문이다. (그 외에도 사실 해당 환경에서 사용하기에 부적절하다고 생각하였다.)
2. Routing DataSource 구현
Spring에서 제공해주는 AbstractRoutingDataSource를 이용하여 DataSource를 라우팅하는 방법이다.
보통 @Trnsaciontal의 readOnly속성에 따라 Master/Slave DataSource를 분기한다.
1. AbstractRoutingDataSource를 상속받는 클래스를 구현
@Slf4j
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
return isReadOnly ? DataSourceConfigConstants.SLAVE_DATASOURCE : DataSourceConfigConstants.MASTER_DATASOURCE;
}
}
여기서 determineCurrentLookupKey는 어떤 DataSource 연결할지 결정하고 Key를 반환해주는 역할을 한다. 현재 Transaction의 ReadOnly 여부를 판단하여 Master/Slave키를 반환하도록 한다.
밑에서 보겠지만, DynamicRoutingDataSource 에서 Map으로 Master/Slave DataSource를 갖고 있으며 determineCurrentLookupKey에서 반환한 Key를 이용하여 사용할 DataSource를 얻는다.
2. DynamicRoutingDataSource(AbstractRoutingDataSource 상속받은 클래스) , LazyDataSource 정의
...
@Bean
public DynamicRoutingDataSource routingDataSource() throws SQLException {
DynamicRoutingDataSource routingDataSource = new DynamicRoutingDataSource();
Map<Object, Object> dataSourceMap = new HashMap<>();
HikariDataSource master = masterDataSource();
dataSourceMap.put(DataSourceConfigConstants.MASTER_DATASOURCE, master);
dataSourceMap.put(DataSourceConfigConstants.SLAVE_DATASOURCE, slaveDataSource());
routingDataSource.setTargetDataSources(dataSourceMap);
routingDataSource.setDefaultTargetDataSource(master);
return routingDataSource;
}
@Bean
public LazyConnectionDataSourceProxy lazyDataSource(DynamicRoutingDataSource routingDataSource) {
return new LazyConnectionDataSourceProxy(routingDataSource);
}
...
DynamicRoutingDataSource를 구현하여 LazyConnectionDataSourceProxy 로 감싼다.
LazyConnectionDataSourceProxy을 통해 DataSource의 커넥션을 가져오는 시점을 늦춰, 우리가 원하는 DataSource를 얻을 수 있게 한다.
DynamicRoutingDataSource는 Master/Slave DataSoure를 Map으로 갖고있으며 요청이 들어오면 적당한 DataSource를 결정(determineCurrentLookupKey)하여 반환한다.
3. TransactionManager, SqlSessionFactory 정의
...
@Bean
public PlatformTransactionManager transactionManager(LazyConnectionDataSourceProxy lazyDataSource) {
return new DataSourceTransactionManager(lazyDataSource);
}
@Bean
public SqlSessionFactoryBean sqlSessionFactoryBean(LazyConnectionDataSourceProxy lazyDataSource, ApplicationContext applicationContext) throws
IOException {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(lazyDataSource);
factoryBean.setConfigLocation(
applicationContext.getResource("classpath:common/mybatis/config.xml"));
factoryBean.setMapperLocations(
applicationContext.getResources("classpath*:common/mybatis/mappers/**/*.xml"));
factoryBean.setTypeHandlersPackage("com.example.sample.config.mybatis");
return factoryBean;
}
끝
사실상 DynamicRoutingDataSource, LazyConnectionDataSourceProxy, HikariDataSource 는 모두 DataSource interface의 구현체이기 때문에 반환 타입을 DataSource로 하여도 된다. 하지만 그렇게되면 애플리케이션 기동시 순환참조 문제가 발생하여, 따로 의존성 설정 작업(@DependsOn)이 필요하여 인터페이스가 아닌 실제 구현체타입을 반환하였다.
이렇게 설정을 한 후 메소드에 @Trnsactional(readOnly=true)를 붙이면 Slave DB에서 쿼리가 실행되는 것을 볼 수 있다.
기존에 Master에서 실행되던 단순 Select 쿼리들을 어노테이션(@Transactional(readOnly=true))추가만으로 Slave에서 실행되게 할 수 있다.
'프로그래밍 노트 > SPRING' 카테고리의 다른 글
[Spring] 프록시 활용 - 프록시 패턴, 데코레이터 패턴 (0) | 2022.09.26 |
---|---|
[Spring] 스프링의 트랜잭션 기술 (0) | 2022.04.16 |
[SpringBatch] JobParameter와 Scope (0) | 2021.12.01 |
[Spring] 스프링MVC 기본 설정(xml, java config) (0) | 2020.12.21 |
[Spring] 스프링이란? Spring MVC 동작방식 (0) | 2020.12.19 |