본문 바로가기
Framework & Lib & API/스프링

AbstractRoutingDataSource에서 Transactional readonly값 false만 리턴하는 오류 해결

by 코딩공장공장장 2024. 6. 16.

문제 상황

connection 경로를 결정하는 AbstractRoutingDataSource에서 현재 실행중인 트랜잭션의 readonly 값을 읽어와 
readonly=true 이면 slave db로 경로를 결정하고, readonly=false이면 master db로 경로를 결정하는데
AbstractRoutingDataSource의 구현체에서 TransactionSynchronizationManager.isCurrentTransactionReadOnly()를 
호출하면 false만 리턴이됨

 

문제 원인 

트랜잭션 범위 내에서 커넥션을 설정하는 AbstractPlatformTransactionManager가 AbstractRoutingDataSource를 먼저 호출하고

TransactionSynchronizationManager에 트랜잭션 definition을 나중에 설정함

 

즉, readonly값을 설정하기 전에 사용하기에 false만 선언됨

(readonly 값이 없으면 false를 리턴함)

 

해결법

AbstractRoutingDataSource의 구현체가 호출되기 전에 실행되는 콜백 객체인 TransactionExecutionListener에서 TransactionStatus를 전달 받아 TransactionSynchronizationManager에 미리 트랜잭션 상태를 설정

@Component
public class TransactionDefSaveExecutionListener implements TransactionExecutionListener {

   public void beforeBegin(@NotNull TransactionExecution transaction) {
      DefaultTransactionStatus defaultTxStatus = (DefaultTransactionStatus)transaction;
      TransactionSynchronizationManager.setCurrentTransactionReadOnly(defaultTxStatus.isReadOnly());
      TransactionSynchronizationManager.setActualTransactionActive(true);
   }
   
}

 

 

소스 분석

AbstractPlatformTransactionManager - startTransaction 메서드

private TransactionStatus startTransaction(TransactionDefinition definition, Object transaction, boolean nested, boolean debugEnabled, @Nullable SuspendedResourcesHolder suspendedResources) {
  boolean newSynchronization = this.getTransactionSynchronization() != 2;
  DefaultTransactionStatus status = this.newTransactionStatus(definition, transaction, true, newSynchronization, nested, debugEnabled, suspendedResources);
  this.transactionExecutionListeners.forEach((listener) -> {
      listener.beforeBegin(status);
  });


  try {
      this.doBegin(transaction, definition);
  } catch (Error | RuntimeException var9) {
      this.transactionExecutionListeners.forEach((listener) -> {
          listener.afterBegin(status, var9);
      });
      throw var9;
  }

  this.prepareSynchronization(status, definition);
  this.transactionExecutionListeners.forEach((listener) -> {
      listener.afterBegin(status, (Throwable)null);
  });
  return status;
}

위 소스의 빨간색 글씨로 된 소스에 주목하자.

 

먼저 두번째 빨간 글씨인 this.doBegin 메서드 부터 보겠다.

 

해당 메서드를 타고 들어가면 AbstractRoutingDataSource의 구현체에서 determinTargetDataSource() 메서드가 호출되고

determinTargetDataSource()  안에서 미리 설정된 dataSource의 키값을 결정하는 determineCurrentLookupKey() 메서드가
실행된다.

protected DataSource determineTargetDataSource() {
    Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
    Object lookupKey = this.determineCurrentLookupKey();
    DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
    if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
        dataSource = this.resolvedDefaultDataSource;
    }

    if (dataSource == null) {
        throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
    } else {
        return dataSource;
    }
}

@Nullable
protected abstract Object determineCurrentLookupKey();

 

 

나의 경우 아래와 같이 읽기작업은 slave와 쓰기작업은 master로 설정하였다.

public class MasterSlaveRoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
    	// 트랜잭션 작업인지 여부
        boolean isTransactionActive = TransactionSynchronizationManager.isActualTransactionActive();
        
        // readonly 트랜잭션인지 여부
        boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly()
        
        if (!isTransactionActive || isReadOnly) {
            return "slave";
        } else {
            return "master";
        }
    }
}

 

반환 값은 미리 설정한 datasource의 키값이어야한다.

 

허나 이 소스에서 

TransactionSynchronizationManager.isActualTransactionActive() 와 TransactionSynchronizationManager.isCurrentTransactionReadOnly() 가 false만 반환한다.

 

이유는 세번째 빨간 글씨로 표현한 this.prepareSynchronization(status, definition);가

 

TransactionSynchronizationManager의 상태를 설정한다.

public abstract class TransactionSynchronizationManager {
    ...
    private static final ThreadLocal<Boolean> currentTransactionReadOnly = new NamedThreadLocal("Current transaction read-only status");
    
    private static final ThreadLocal<Boolean> actualTransactionActive = new NamedThreadLocal("Actual transaction active");
	...
}

 

참고로 TransactionSynchronizationManager를 보면 readonly와 active 여부를 스레드 로컬 공간에 저장한다.

 

클라이언트마다 할당받은 스레드의 고유한 공간에 상태 값을 저장하기에

클라이언트마다 트랜잭션 상태 값을 독립적으로 스레드에 갖고 있는 것을 알 수 있다.

 

다시 AbstractPlatformTransactionManager의 startTransaction 메서드로 돌아와서 

 

실행 순서를 보면 doBegin이 먼저 prepareSynchronization이 그 이후에 실행 된다.

 

스프링이 TransactionSynchronizationManager의 상태를 설정하기 전에

TransactionSynchronizationManager의 상태를 가져와서 사용하려다 보니 계속해서 false만 리턴하는 것이다.

 

이에 대한 해결 방법이 첫번째 빨간 글씨로 표현한

 

this.transactionExecutionListeners.forEach((listener) -> { listener.beforeBegin(status); });

 

부분이다.

 

이 소스코드가 해결법이 될 수 있는 이유는 beforeBegin이라는 메서드에 status를 넣어주고 있기 때문이다.

 

우리가 필요한 해결법은 doBegin에 의해 AbstractRoutingDatasource가 호출될 때 현재 진행 중인 트랜잭션 상태를 가져와야하는 것이다.

 

스프링이 스레드 로컬 공간을 사용하는 TransactionSynchronizationManager에 트랜잭션 상태를 저장해주기 전에는 

 

트랜잭션 상태는 메서드 파라미터로만 전달된다.

 

 

 

위와 같이 최초에 reflection을 통해 전달받은 타겟 메서드와 클래스 정보를 통해 트랜잭션 상태를 생성하고

 

그 이후 AbstractPlatformTransactionManager의 startTransaction 메서드로 전달되기까지

 

메서드의 파라미터로만 전달되고 다른 곳에 저장시켜주는 곳이 없다. 

 

별도의 공간인 스레드 로컬에 저장시켜주는 로직이 AbstractRoutingDatasource 보다 이후에 실행되니 

 

마땅히 참조할 만한 저장 공간이 없는 것이다. 

 

별도의 참조할만한 객체는 없지만

this.transactionExecutionListeners.forEach((listener) -> { listener.beforeBegin(status); });

를 통해 콜백 객체의 메서드 파라미터에 트랜잭션 상태를 전달해주고 있다. 

 

이 콜백 객체를 사용한다면  스프링 디폴트 프로세스를 그대로 따라갈 수 있다. 

 

따라서 아래와 같은 해결법이 나오게 된 것이다.

 

@Component
public class TransactionDefSaveExecutionListener implements TransactionExecutionListener {

   public void beforeBegin(@NotNull TransactionExecution transaction) {
      DefaultTransactionStatus defaultTxStatus = (DefaultTransactionStatus)transaction;
      TransactionSynchronizationManager.setCurrentTransactionReadOnly(defaultTxStatus.isReadOnly());
      TransactionSynchronizationManager.setActualTransactionActive(true);
   }
   
}

 

과거에 토비의 스프링이라는 책을 읽으며 커넥션을 메서드 파라미터로 계속해서 공유하는 패턴에서 스레드 로컬 공간에 저장하여

스레드 로컬에서 현재 커넥션을 가져와 커넥션을 계속 파라미터로 넘겨줘야하는 지저분한 코드 패턴을 없애 나갔던 것을 봤던 기억이있다.

 

그래서 그런지 이번에 트러블 슈팅을 하며 트랜잭션 상태는 사용자마다 고유하게 갖고 있어야하므로 

 

메서드 파라미터로 지속적으로 전달 되고 어느 시점에 분명 스레드 로컬 공간에 저장하는 로직이 있을 거라고 판단하고 

 

디버깅을 하니 굉장히 수월하게 해결하였다.

 

다행스럽게 콜백 객체를 통해 상태를 넘겨주는 로직까지 스프링에서 제공해주고 있으니 해결법 또한 단순하게 처리할 수 있었다.

 

반응형