Spring AbstractRoutingDataSource의 DataSource가 바뀌지 않는 문제

ddindo·2022년 11월 22일
0
post-thumbnail

환경 구성

Postgresql 12 버전 Database 2개
Java 8
Spring-boot 2.2.9
Maven 3.6.3
Mybatis 2.1.3

사전 지식

  • AbstractRoutingDataSource
  • ThreadLocal

발생한 문제

현재 시스템에서 AbstractRoutingDataSource를 사용하여 2개의 Postgresql DB를 동적으로 라우팅하여 사용하고 있다.

원하는 데이터를 제공하기 위해서 두 개의 DB를 모두 조회해야 하지만, 데이터 소스가 중간에 변경되지 않는 현상이 발견 됐다.

기존 구조는 Controller -> Service -> Mapper 를 통해 DB를 접근하고 있다.
이 때, Service는 Trasnaction 처리가 되어 있었다. 나는 Controller에서 부수적인 동작을 최대한 피하기 위해 Service에서 DataSource를 바꾸고자 하였다. 그래서 새로운 sqlsession을 생성하고 datasoruce를 변경하려고 했다. 하지만 아무리 바꾸려 해도 기존에 설정한 DataSource를 통해 접근하는 문제가 발생했고, 이를 해결하기 위해 조사한 내용을 정리하였다.

문제 발생 이유

우선 첫 번째로 Servce에서 ThreadLocal의 값을 바꾸려고 했던 것으로 돌아가보자,
해당 Service는 Transactional을 사용하여 Transaction 처리가 진행된다. 여기서 문제점이 발생했다.

Transactional로 Transaction을 처리하게 되면 Spring에서는 Proxy를 통해 다음과 같은 구조로 메서드가 변경이 된다. (실제 코드는 아니도 단순화한 구조이다.)

public void proxyTargetMethod() {
	TransactionManager.getTransactino();
    TransactionManager.dobegin();
    targetMethod();
    TransactionManager.commit();
}

위 코드의 transactionManager는 Transaction을 관리하기 하기 위해 만들어진 클래스로
가장 기본적인 PlatformTransactionManager 인터페이스를 통해 구현한다.

내가 사용한 TransactionManager는 DataSourceTransactionManager로 pg JDBC를 사용하기 때문에 이를 사용하고 있었다.

그리고 아래는 DataSourceTransactinoManager에서 트랜잭션을 가져오는 코드이다.

	@Override
	protected Object doGetTransaction() {
		DataSourceTransactionObject txObject = new DataSourceTransactionObject();
		txObject.setSavepointAllowed(isNestedTransactionAllowed());
		ConnectionHolder conHolder =
				(ConnectionHolder) TransactionSynchronizationManager.getResource(obtainDataSource());
		txObject.setConnectionHolder(conHolder, false);
		return txObject;
	}

코드의 구조를 보면 TransactinoSynchronizationManager라는 것을 볼 수 있다. 해당 클래스는 Transaction의 동기화 관련 처리를 담당하는 클래스로 DataSourceTransactinoManger는 이 클래스로 부터 ConnectionHolder를 얻어온다.

다음으로 doBegin() 메서드이다.

	@Override
	protected void doBegin(Object transaction, TransactionDefinition definition) {
		DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
		Connection con = null;

		try {
			if (!txObject.hasConnectionHolder() ||
					txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
				Connection newCon = obtainDataSource().getConnection();
				if (logger.isDebugEnabled()) {
					logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
				}
				txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
			}

			txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
			con = txObject.getConnectionHolder().getConnection();

			Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
			txObject.setPreviousIsolationLevel(previousIsolationLevel);
			txObject.setReadOnly(definition.isReadOnly());

			// Switch to manual commit if necessary. This is very expensive in some JDBC drivers,
			// so we don't want to do it unnecessarily (for example if we've explicitly
			// configured the connection pool to set it already).
			if (con.getAutoCommit()) {
				txObject.setMustRestoreAutoCommit(true);
				if (logger.isDebugEnabled()) {
					logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
				}
				con.setAutoCommit(false);
			}

			prepareTransactionalConnection(con, definition);
			txObject.getConnectionHolder().setTransactionActive(true);

			int timeout = determineTimeout(definition);
			if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
				txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
			}

			// 여기서 ThreadLocal에 값을 저장
			if (txObject.isNewConnectionHolder()) {
				TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
			}
		}

		catch (Throwable ex) {
			if (txObject.isNewConnectionHolder()) {
				DataSourceUtils.releaseConnection(con, obtainDataSource());
				txObject.setConnectionHolder(null, false);
			}
			throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
		}
	}

해당 메서드를 통해 TransactionSynchroniationManager의 ThreadLocal에 Connection을 설정한다.
Key는 RoutingDataSource의 해시값이고 Value는 Connection 객체이다.

이렇게 된 후에 SqlSession에서는 쿼리 명령을 수행하게 되는데, 이 때 SqlSession이 바라보는 connection은 RoutingDataSource의 Conntion이 아니라 Transaction의 시작 과정에서 가져온 connection을 바라보게 된다. 그래서 RoutingDataSource의 Connection을 변경하고 실행해도 다른 디비에 접근하게 되는 것이다.

해결 방법

이렇게 분산된 DataSource를 Transaction으로 묶어 사용하기 위해서는 기존에 사용하던 DataSourceTransactionManager를 사용하는 것이 아니라 JtaTransactionManager를 사용해야 한다.

+ 회사 코드를 직접 올릴 수가 없기 때문에 이를 추후에 따로 정리해서 포스팅 하도록 하겠다.

Reference

https://d2.naver.com/helloworld/5812258
https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=aeroviper&logNo=221203994541

0개의 댓글