트랜잭션 readOnly 변경이 가능할까?

허준현·2024년 2월 24일
0

Spring

목록 보기
3/3
post-thumbnail

현재 프로젝트를 진행하면서 Mysql 에서 master-slave 구조를 접하게 되면서 겪었던 문제 때문에 이 글을 작성하게 되었다. 전 프로젝트에서도 동일하게 해당 구조를 사용하였으나 도메인 서비스별로 구분하여 Datasource를 나누어서 사용하고 있었다.
현재 프로젝트에서는 dml문을 사용하는 경우에는 master, select 절에는 slave를 사용하고자 조회 서비스에 @Transactional(readonly=true)를 붙여서 사용하고 있었다.

해당 어노테이션별로 다르게 connection이 생성될까?

문득 master 테이블에 insert를 진행하고나서 slave에서 select 를 하게 되는 경우 Replication이 발생하지 않으면 해당 insert를 못 가져올 것 같아 찾아봤다.

우선적으로 해당 구조를 만들기 위해서는 기본적으로 알아야 하는 트랜잭션 시작시 일어나는 과정들과 AbstractRoutingDataSourceLazyConnectionDataSourceProxy 에 대해서 간단하게 알아보자.

트랜잭션 시작시 일련 과정

스프링에서 JDBC 트랜잭션을 어떻게 시작하고 언제 commit, rollback 되는지 알아야 한다.
순수 jdbc 만을 사용한다고 하면 서비스 로직 부분에 connection 맺는 부분과 setAutocommit(true, false) 를 지정하여 처리하게 되는데 스프링 @Transactional을 사용하게 된다면 이부분을 자동적으로 Proxy로 적용해준다. 따라서 트랜잭션을 시작하게 되면
트랜잭션 시작-> 커넥션 획득 -> 비지니스 로직 -> 트랜잭션 종료 라는 과정을 거치게 된다.

AbstractRoutingDataSource

아래의 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";
        }
    }
}

LazyConnectionDataSourceProxy

아래의 코드에서 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);
    }

그래서 readonly 값을 바꾸면 어떻게 되는데?

해당 인프런 질문글 및 위의 과정을 통해 알 수 있듯이 새로운 트랜잭션을 시작하는 시점(커넥션을 획득하고 트랜잭션 시작하는 경우)에 최초 옵션만 적용되며 나머지 옵션들에 대해서는 무시된다.
따라서 master-slave 구조를 잘 나누기 위해서는 도메인 혹은 서비스별로 비지니스 로직을 구분한 뒤에 적합한 DB Datasource 를 가져오도록 하자.

결론

  • 처음 실행된 트랜잭션의 readonly 옵션은 변경되지 않으므로 초기 세팅으로 해당 db를 조회하게 된다.
  • master-slave 구조에서 LazyConnectionDataSourceProxy를 사용하지 않게 된다면 master DB에서만 조회하게 될 것이다.
  • LazyConnectionDataSourceProxy는 db접속오류가 발생하더라도 앱 구동에 영향을 주지 않게 하거나, 사용하지 않은 커넥션을 유지하는 것을 방지하기 위해 사용하지만 위와 같은 이유로도 사용된다.

참조 :

Mysql slave 구조 설계 시 호출 방안
트랜잭션 시작시 일련 과정

profile
best of best

1개의 댓글

comment-user-thumbnail
2024년 2월 24일

유익한 포스트 잘 보고갑니다:)

답글 달기