공부/프로그래밍

[spring] jpa와 mybatis 동시 사용시 transactinoManager (multi) 설정하기(xml) 및 내부 살펴보기

demonic_ 2019. 12. 17. 07:00

현재 시스템이 2개의 TransactionManager 로 나뉘어있다.

하나는 지금까지 사용한 마이바티스 기반 TransactionManager가,

그리고 다른 하나는 앞으로 JPA로 커스터마이징 할 TransactionManager 이다.

 

기존의 마이바티스로 설정된 TransactionManager 의 구현클래스는 DataSourceTransactionManager 이다

<bean id="transactionManager"
	  class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
	<property name="dataSource" ref="dataSourceMySQL" />
</bean>

 

그리고 JPA의 경우 구현하는 TransactionManager는 JpaTransactionManager 다

<bean id="jpaTransactionManager" 
      class="org.springframework.orm.jpa.JpaTransactionManager">
	<property name="entityManagerFactory" ref="jpaEntityManagerFactory" />
</bean>

 

문제는 @Transactional 은 하나의 TransactionManager 를 사용한다는 점이다.

 

그래서 @Transactional 을 선언한 곳에서 어떤트랜잭션을 사용하느냐에 따라 어떤것은 롤백이 되고 어떤것은 롤백이 되지 않는다.

참고로 @Transactional에 Bean을 선언하는 방법은 value 를 넣어주면 된다

지정하지 않는다면 기본참조 Bean 이름은 'transactionManager' 다

@Transactional(value="jpaTransactionManager")
public void multiTxMethod() {
   ...(내용)
}

 

@Transactional 어노테이션 내부

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    @AliasFor("transactionManager")  // 기본값으로 transactionManager를 사용한다
    String value() default "";

    @AliasFor("value")
    String transactionManager() default "";

    Propagation propagation() default Propagation.REQUIRED;

...

 

 

그런데 이것들을 하나로 묶어줄 수 있는 방법이 있다. 바로 ChainedTransactionManager 다.

 

설정은 다음과 같이 할 수 있다.

<bean id="transactionManager" class="org.springframework.data.transaction.ChainedTransactionManager">
	<constructor-arg>
		<list>
			<ref bean="mybatisTransactionManager"/>
			<ref bean="jpaTransactionManager"/>
		</list>
	</constructor-arg>
</bean>

 

다만 ChainedTransactionManager을 기본으로 사용하려면 기존의 transactionManager bean 이름을 변경해주어야 한다.

(ChainedTransactionManager 가 transactionManager 이름을 사용하도록 해야하기 때문)

여기서는 mybatisTransactionManager로 지정했다.

<!-- 변경전 -->
<!--<bean id="transactionManager"-->
<!--	  class="org.springframework.jdbc.datasource.DataSourceTransactionManager">-->
<!--	<property name="dataSource" ref="dataSourceMySQL" />-->
<!--</bean>-->

<!-- 변경후-->
<bean id="mybatisTransactionManager"
	class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
	<property name="dataSource" ref="dataSourceMySQL" />
</bean>

 

 

 

# 작동원리 살펴보기

클래스 내부를 들여다보면 클래스가 생성될 때 적용할 트랜잭션 묶음을 등록한다.

public ChainedTransactionManager(PlatformTransactionManager... transactionManagers) {
    this(SpringTransactionSynchronizationManager.INSTANCE, transactionManagers);
}

 

살펴보면 DataSourceTransactionManager와 JpaTransactionManager 를 리스트로 받는다.

 

코드가 실행되고 나면 검토 후 trasactionManagers 에 등록된다.

ChainedTransactionManager(SynchronizationManager synchronizationManager, PlatformTransactionManager... transactionManagers) {
    Assert.notNull(synchronizationManager, "SynchronizationManager must not be null!");
    Assert.notNull(transactionManagers, "Transaction managers must not be null!");
    Assert.isTrue(transactionManagers.length > 0, "At least one PlatformTransactionManager must be given!");
    this.synchronizationManager = synchronizationManager;
    this.transactionManagers = Arrays.asList(transactionManagers);
}

 

이후 트랜잭션이 필요하면 ChainedTransactionManager 내 getTransaction 메서드가 실행되면서 각각의 TransactionManager 에서 트랜잭션을 획득한다.

public class ChainedTransactionManager implements PlatformTransactionManager {
   ...
    public MultiTransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException {
        MultiTransactionStatus mts = new MultiTransactionStatus((PlatformTransactionManager)this.transactionManagers.get(0));
        if (definition == null) {
            return mts;
        } else {
            if (!this.synchronizationManager.isSynchronizationActive()) {
                this.synchronizationManager.initSynchronization();
                mts.setNewSynchonization();
            }

            try {
                Iterator var3 = this.transactionManagers.iterator();

                while(var3.hasNext()) {
                    PlatformTransactionManager transactionManager = (PlatformTransactionManager)var3.next();
                    // 이 부분을 실행하면서 각 TransactionManager 의 트랜잭션 생성을 실행한다.
                    mts.registerTransactionManager(definition, transactionManager);
                }
   ...
# DataSourceTransactionManager

public class DataSourceTransactionManager extends AbstractPlatformTransactionManager implements ResourceTransactionManager, InitializingBean {
    ...
    protected void doBegin(Object transaction, TransactionDefinition definition) {
        DataSourceTransactionManager.DataSourceTransactionObject txObject = (DataSourceTransactionManager.DataSourceTransactionObject)transaction;
        Connection con = null;

        try {
            if (!txObject.hasConnectionHolder() || txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
                Connection newCon = this.obtainDataSource().getConnection();
                if (this.logger.isDebugEnabled()) {
                    this.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);
            if (con.getAutoCommit()) {
                txObject.setMustRestoreAutoCommit(true);
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
                }

                con.setAutoCommit(false);
            }

            this.prepareTransactionalConnection(con, definition);
            txObject.getConnectionHolder().setTransactionActive(true);
            int timeout = this.determineTimeout(definition);
            if (timeout != -1) {
                txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
            }

            if (txObject.isNewConnectionHolder()) {
                TransactionSynchronizationManager.bindResource(this.obtainDataSource(), txObject.getConnectionHolder());
            }

        } catch (Throwable var7) {
            if (txObject.isNewConnectionHolder()) {
                DataSourceUtils.releaseConnection(con, this.obtainDataSource());
                txObject.setConnectionHolder((ConnectionHolder)null, false);
            }

            throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", var7);
        }
    }
...
# JpaTransactionManager

public class JpaTransactionManager extends AbstractPlatformTransactionManager implements ResourceTransactionManager, BeanFactoryAware, InitializingBean {
    ...
    protected void doBegin(Object transaction, TransactionDefinition definition) {
        JpaTransactionManager.JpaTransactionObject txObject = (JpaTransactionManager.JpaTransactionObject)transaction;
        if (txObject.hasConnectionHolder() && !txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
            throw new IllegalTransactionStateException("Pre-bound JDBC Connection found! JpaTransactionManager does not support running within DataSourceTransactionManager if told to manage the DataSource itself. It is recommended to use a single JpaTransactionManager for all transactions on a single DataSource, no matter whether JPA or JDBC access.");
        } else {
            try {
                EntityManager em;
                if (!txObject.hasEntityManagerHolder() || txObject.getEntityManagerHolder().isSynchronizedWithTransaction()) {
                    em = this.createEntityManagerForTransaction();
                    if (this.logger.isDebugEnabled()) {
                        this.logger.debug("Opened new EntityManager [" + em + "] for JPA transaction");
                    }

                    txObject.setEntityManagerHolder(new EntityManagerHolder(em), true);
                }

                em = txObject.getEntityManagerHolder().getEntityManager();
                int timeoutToUse = this.determineTimeout(definition);
                Object transactionData = this.getJpaDialect().beginTransaction(em, new JpaTransactionManager.JpaTransactionDefinition(definition, timeoutToUse, txObject.isNewEntityManagerHolder()));
                txObject.setTransactionData(transactionData);
                if (timeoutToUse != -1) {
                    txObject.getEntityManagerHolder().setTimeoutInSeconds(timeoutToUse);
                }

                if (this.getDataSource() != null) {
                    ConnectionHandle conHandle = this.getJpaDialect().getJdbcConnection(em, definition.isReadOnly());
                    if (conHandle != null) {
                        ConnectionHolder conHolder = new ConnectionHolder(conHandle);
                        if (timeoutToUse != -1) {
                            conHolder.setTimeoutInSeconds(timeoutToUse);
                        }

                        if (this.logger.isDebugEnabled()) {
                            this.logger.debug("Exposing JPA transaction as JDBC [" + conHandle + "]");
                        }

                        TransactionSynchronizationManager.bindResource(this.getDataSource(), conHolder);
                        txObject.setConnectionHolder(conHolder);
                    } else if (this.logger.isDebugEnabled()) {
                        this.logger.debug("Not exposing JPA transaction [" + em + "] as JDBC transaction because JpaDialect [" + this.getJpaDialect() + "] does not support JDBC Connection retrieval");
                    }
                }

                if (txObject.isNewEntityManagerHolder()) {
                    TransactionSynchronizationManager.bindResource(this.obtainEntityManagerFactory(), txObject.getEntityManagerHolder());
                }

                txObject.getEntityManagerHolder().setSynchronizedWithTransaction(true);
            } catch (TransactionException var9) {
                this.closeEntityManagerAfterFailedBegin(txObject);
                throw var9;
            } catch (Throwable var10) {
                this.closeEntityManagerAfterFailedBegin(txObject);
                throw new CannotCreateTransactionException("Could not open JPA EntityManager for transaction", var10);
            }
        }
    }
    ...

 

각 클래스에서 생성한 트랜잭션을 org.springframework.data.transaction.MultiTransactionStatus.class 에서 관리한다.

package org.springframework.data.transaction;
...

class MultiTransactionStatus implements TransactionStatus {
    private final PlatformTransactionManager mainTransactionManager;
    // 트랜잭션이 여기에 담겨진다.
    private final Map<PlatformTransactionManager, TransactionStatus> transactionStatuses = Collections.synchronizedMap(new HashMap());
    private boolean newSynchonization;

    ...
    
    // 이 부분이 실행되면서 각 생성된 트랜잭션을 추가한다.
    public void registerTransactionManager(TransactionDefinition definition, PlatformTransactionManager transactionManager) {
        this.getTransactionStatuses().put(transactionManager, transactionManager.getTransaction(definition));
    }

    ...

 

 

임의로 코드에서 에러를 발생했더니 ChainedTransactionManager 클래스 내 rollback 메서드를 호출한다

 

transactionManagers 를 반복문을 통해 rollback 을 차례로 호출한다.

 

과정:

ChainedTransactionManager.class -> MultiTransactionStatus.class -> PlatformTransactionManager.class -> AbstractPlatformTransactionManager.class -> 구현된(DataSourceTrasactionManager 또는 JpaTransactionManager) TransactionManager 의 doRollback 메서드 호출

# ChainedTransactionManager

    public void rollback(TransactionStatus status) throws TransactionException {
        Exception rollbackException = null;
        PlatformTransactionManager rollbackExceptionTransactionManager = null;
        MultiTransactionStatus multiTransactionStatus = (MultiTransactionStatus)status;
        Iterator var5 = this.reverse(this.transactionManagers).iterator();

        while(var5.hasNext()) {
            PlatformTransactionManager transactionManager = (PlatformTransactionManager)var5.next();

            try {
                // 롤백수행
                multiTransactionStatus.rollback(transactionManager);
            } catch (Exception var8) {
                if (rollbackException == null) {
                    rollbackException = var8;
                    rollbackExceptionTransactionManager = transactionManager;
                } else {
                    LOGGER.warn("Rollback exception (" + transactionManager + ") " + var8.getMessage(), var8);
                }
            }
        }

        if (multiTransactionStatus.isNewSynchonization()) {
            this.synchronizationManager.clearSynchronization();
        }

        if (rollbackException != null) {
            throw new UnexpectedRollbackException("Rollback exception, originated at (" + rollbackExceptionTransactionManager + ") " + rollbackException.getMessage(), rollbackException);
        }
    }
# MultiTransactionStatus

    public void rollback(PlatformTransactionManager transactionManager) {
        transactionManager.rollback(this.getTransactionStatus(transactionManager));
    }
# (interface) PlatformTransactionManager

package org.springframework.transaction;

import org.springframework.lang.Nullable;

public interface PlatformTransactionManager {
    TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;

    void commit(TransactionStatus var1) throws TransactionException;
    // rollback 호출
    void rollback(TransactionStatus var1) throws TransactionException;
}
# AbstractPlatformTransactionManager

public abstract class AbstractPlatformTransactionManager implements PlatformTransactionManager, Serializable {
    ...
    public final void rollback(TransactionStatus status) throws TransactionException {
        if (status.isCompleted()) {
            throw new IllegalTransactionStateException("Transaction is already completed - do not call commit or rollback more than once per transaction");
        } else {
            DefaultTransactionStatus defStatus = (DefaultTransactionStatus)status;
            // 이부분 실행.
            this.processRollback(defStatus, false);
        }
    }
    ...


    // 실제 롤백을 수행
    private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
        try {
            boolean unexpectedRollback = unexpected;

            try {
                this.triggerBeforeCompletion(status);
                if (status.hasSavepoint()) {
                    if (status.isDebug()) {
                        this.logger.debug("Rolling back transaction to savepoint");
                    }

                    status.rollbackToHeldSavepoint();
                } else if (status.isNewTransaction()) {
                    if (status.isDebug()) {
                        this.logger.debug("Initiating transaction rollback");
                    }

                    this.doRollback(status); // 롤백수행 호출
                } else {
                    if (status.hasTransaction()) {
                        if (!status.isLocalRollbackOnly() && !this.isGlobalRollbackOnParticipationFailure()) {
                            if (status.isDebug()) {
                                this.logger.debug("Participating transaction failed - letting transaction originator decide on rollback");
                            }
                        } else {
                            if (status.isDebug()) {
                                this.logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
                            }

                            this.doSetRollbackOnly(status);
                        }
                    } else {
                        this.logger.debug("Should roll back transaction but cannot - no transaction available");
                    }

                    if (!this.isFailEarlyOnGlobalRollbackOnly()) {
                        unexpectedRollback = false;
                    }
                }
            } catch (Error | RuntimeException var8) {
                this.triggerAfterCompletion(status, 2);
                throw var8;
            }

            this.triggerAfterCompletion(status, 1);
            if (unexpectedRollback) {
                throw new UnexpectedRollbackException("Transaction rolled back because it has been marked as rollback-only");
            }
        } finally {
            this.cleanupAfterCompletion(status);
        }

    }

 

각 클래스의 doRollback 메서드

# DataSourceTransactionManager.class

    ...
    protected void doRollback(DefaultTransactionStatus status) {
        DataSourceTransactionManager.DataSourceTransactionObject txObject = (DataSourceTransactionManager.DataSourceTransactionObject)status.getTransaction();
        Connection con = txObject.getConnectionHolder().getConnection();
        if (status.isDebug()) {
            this.logger.debug("Rolling back JDBC transaction on Connection [" + con + "]");
        }

        try {
            con.rollback();
        } catch (SQLException var5) {
            throw new TransactionSystemException("Could not roll back JDBC transaction", var5);
        }
    }
    ...
# JpaTransactionManager.class

    ...
    protected void doRollback(DefaultTransactionStatus status) {
        JpaTransactionManager.JpaTransactionObject txObject = (JpaTransactionManager.JpaTransactionObject)status.getTransaction();
        if (status.isDebug()) {
            this.logger.debug("Rolling back JPA transaction on EntityManager [" + txObject.getEntityManagerHolder().getEntityManager() + "]");
        }

        try {
            EntityTransaction tx = txObject.getEntityManagerHolder().getEntityManager().getTransaction();
            if (tx.isActive()) {
                tx.rollback();
            }
        } catch (PersistenceException var7) {
            throw new TransactionSystemException("Could not roll back JPA transaction", var7);
        } finally {
            if (!txObject.isNewEntityManagerHolder()) {
                txObject.getEntityManagerHolder().getEntityManager().clear();
            }

        }
    }
    ...

 

 

롤백순서는 처음 TransactionManager 를 등록한 순서의 반대로(LIFO) 실행된다.

여기서는 mybatisTransactionManager -> jpaTransactionManager 순서로 등록했기때문에

롤백은 jpaTransactionManager -> mybatisTransactionManager 순으로 실행한다.

 

내부로직을 보면 트랜잭션을 얻어올때, commit, rollback 을 위해 여러 트랜잭션을 임의의 객체에 넣어둔다.

그래서 반드시 2개이상의 transactionManager 가 필요한게 아니라면 가급적 1개의 트랜잭션을 value 에다 지정해서 사용하는 것을 권장한다.

 

끝.