현재 시스템이 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 에다 지정해서 사용하는 것을 권장한다.
끝.
'공부 > 프로그래밍' 카테고리의 다른 글
[gitlab-telegram] gitlab 의 메세지를 telegram으로 받기 (0) | 2019.12.20 |
---|---|
[docker] 로그 logrotate 로 관리하기(주기적으로 삭제하기) (0) | 2019.12.18 |
[spring] LocalDateTime 주고받기(Response, Request) (0) | 2019.12.15 |
[spring] 데이터 변수명 변경하여 전달하기 (@JsonProperty 사용) (0) | 2019.12.14 |
[spring, jpa] Could not build ClassFile 에러 (0) | 2019.11.27 |
댓글