현재 프로젝트를 진행하면서 Mysql 에서 master-slave 구조를 접하게 되면서 겪었던 문제 때문에 이 글을 작성하게 되었다. 전 프로젝트에서도 동일하게 해당 구조를 사용하였으나 도메인 서비스별로 구분하여 Datasource를 나누어서 사용하고 있었다.
현재 프로젝트에서는 dml문을 사용하는 경우에는 master, select 절에는 slave를 사용하고자 조회 서비스에 @Transactional(readonly=true)를 붙여서 사용하고 있었다.
문득 master 테이블에 insert를 진행하고나서 slave에서 select 를 하게 되는 경우 Replication이 발생하지 않으면 해당 insert를 못 가져올 것 같아 찾아봤다.
우선적으로 해당 구조를 만들기 위해서는 기본적으로 알아야 하는 트랜잭션 시작시 일어나는 과정들과 AbstractRoutingDataSource 와 LazyConnectionDataSourceProxy 에 대해서 간단하게 알아보자.
스프링에서 JDBC 트랜잭션을 어떻게 시작하고 언제 commit, rollback 되는지 알아야 한다.
순수 jdbc 만을 사용한다고 하면 서비스 로직 부분에 connection 맺는 부분과 setAutocommit(true, false) 를 지정하여 처리하게 되는데 스프링 @Transactional을 사용하게 된다면 이부분을 자동적으로 Proxy로 적용해준다. 따라서 트랜잭션을 시작하게 되면
트랜잭션 시작-> 커넥션 획득 -> 비지니스 로직 -> 트랜잭션 종료 라는 과정을 거치게 된다.
아래의 AbstractRoutingDataSource의 determineCurrentLookupKey 메서드는 DataSource 연결이 필요할때마다 DataSourceMap에서 어떤 DataSource를 사용할지 Key를 찾아주는 역할을 하는데 TransactionSynchronizationManager.isCurrentTransactionReadOnly()을 통해서 현재 진행중인 트랜잭션이 ReadOnly 인지 여부를 판단하여 Master / Slave DataSource를 분기 할 수 있다. 여기서 중요하게 볼 것은 일반적인 트랜잭션에서 determineCurrentLookupKey 메소드가 언제 불러오는지가 관건이다.
public class RoutingDataSource extends AbstractRoutingDataSource
{
@Override
protected Object determineCurrentLookupKey() {
if(TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
return "slave";
} else {
return "master";
}
}
}
아래의 코드에서 DataSource 생성 시에 LazyConnection으로 감싸주는 걸 볼 수 있다.
트랜잭션 시작-> 커넥션 획득 -> 비지니스 로직 -> 트랜잭션 종료 로직 안에서 커넥션 획득을 할 때에 spring은 어떤 DB에 커넥션을 맺는지 알아야 한다. 하지만 AbstractRoutingDataSource
를 사용하게 되면 @Transactional() 어노테이션이 작동하기 전에 먼저 커넥션을 가져오기 때문에 default 값인 master DB에 커넥션을 얻어 로직을 수행하게 된다. 따라서 LazyConnectionDataSourceProxy
는 커넥션 프록시 객체를 생성하여 커넥션 가져오는 부분을 지연시켜 isCurrentTransactionReadOnly 값이 올바른 값을 가져오도록 도와주는 역할을 하게 된다.
@Bean
public DataSource routingDataSource() {
DataSource masterDataSource = createDataSource(masterDataSourceProperty);
DataSource slaveDataSource = createDataSource(slaveDataSourceProperty);
RoutingDataSource routingDataSource = new RoutingDataSource();
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put("master", masterDataSource);
dataSourceMap.put("slave", slaveDataSource);
routingDataSource.setTargetDataSources(dataSourceMap);
routingDataSource.setDefaultTargetDataSource(masterDataSource);
return routingDataSource;
}
@DependsOn({"routingDataSource"})
@Bean
public DataSource dataSource(DataSource routingDataSource) {
return new LazyConnectionDataSourceProxy(routingDataSource);
}
해당 인프런 질문글 및 위의 과정을 통해 알 수 있듯이 새로운 트랜잭션을 시작하는 시점(커넥션을 획득하고 트랜잭션 시작하는 경우)에 최초 옵션만 적용되며 나머지 옵션들에 대해서는 무시된다.
따라서 master-slave 구조를 잘 나누기 위해서는 도메인 혹은 서비스별로 비지니스 로직을 구분한 뒤에 적합한 DB Datasource 를 가져오도록 하자.
- 처음 실행된 트랜잭션의 readonly 옵션은 변경되지 않으므로 초기 세팅으로 해당 db를 조회하게 된다.
- master-slave 구조에서 LazyConnectionDataSourceProxy를 사용하지 않게 된다면 master DB에서만 조회하게 될 것이다.
- LazyConnectionDataSourceProxy는 db접속오류가 발생하더라도 앱 구동에 영향을 주지 않게 하거나, 사용하지 않은 커넥션을 유지하는 것을 방지하기 위해 사용하지만 위와 같은 이유로도 사용된다.
유익한 포스트 잘 보고갑니다:)