(JPA) Readonly 트랜잭션은 트랜잭션을 시작하지만 flush를 하지 않는다.
3줄 요약
- @Transaction(readOnly = true)로 설정해도 트랜잭션은 시작된다. (transaction isolation level 보장)
- readOnly 트랜잭션도 시작한 트랜잭션을 종료시켜야하기 때문에 커밋도 한다.
- readOnly 트랜잭션의 Hibernate Session의 FlushMode는 Manual로 강제하기 때문에 트랜잭션을 커밋하기 전에 flush를 하지 않는다. (readOnly 보장)
@Transaction(readOnly = true)로 설정해도 트랜잭션은 시작된다.
1 | public interface SomeEntityRepository extends JpaRepository<Parent, Long> { |

repository의 구현체는 프록시 객체로써 인터페이스이기 때문에 jdk dynamic 프록시 객체가 생성이 된다.
또한 TransactionInterceptor라는 Advisor를 가지고 있으며
- TransactionInterceptor.invoke()
- TransactionAspectSupport.invokeWithinTransaction()
- TransactionAspectSupport.createTransactionIfNecessary()
- AbstractPlatformTransactionManager.getTransaction()
- AbstractPlatformTransactionManager.startTransaction()
- JpaTransactionManager.doBegin()
- HibernateJpaDialect.beginTransaction()
- TransactionImpl.begin()
- JdbcResourceLocalTransactionCoordinatorImpl.TransactionDriverControlImpl.begin()
- LogicalConnectionManagedImpl.begin()
- AbstractLogicalConnectionImplementor.begin()
위와 같은 메서드 호출을 통해서 실제로 트랜잭션을 시작하게 된다.
@Transaction(readOnly = true)에 의해 시작된 트랜잭션은 flush를 하지 않는다.
HibernateJpaDialect.beginTransaction() 을 타고 보다보면 아래와 같은 흐름을 따라가게 된다.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public Object beginTransaction(EntityManager entityManager, TransactionDefinition definition)
throws PersistenceException, SQLException, TransactionException {
Session session = getSession(entityManager);
if (definition.getTimeout() != TransactionDefinition.TIMEOUT_DEFAULT) {
session.getTransaction().setTimeout(definition.getTimeout());
}
boolean isolationLevelNeeded = (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT);
Integer previousIsolationLevel = null;
Connection preparedCon = null;
if (isolationLevelNeeded || definition.isReadOnly()) {
if (this.prepareConnection) {
preparedCon = HibernateConnectionHandle.doGetConnection(session);
previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(preparedCon, definition);
}
else if (isolationLevelNeeded) {
throw new InvalidIsolationLevelException(getClass().getSimpleName() +
" does not support custom isolation levels since the 'prepareConnection' flag is off.");
}
}
// Standard JPA transaction begin call for full JPA context setup...
entityManager.getTransaction().begin();
// Adapt flush mode and store previous isolation level, if any.
FlushMode previousFlushMode = prepareFlushMode(session, definition.isReadOnly());
// ...
}
protected FlushMode prepareFlushMode(Session session, boolean readOnly) throws PersistenceException {
FlushMode flushMode = (FlushMode) ReflectionUtils.invokeMethod(getFlushMode, session);
Assert.state(flushMode != null, "No FlushMode from Session");
if (readOnly) {
// We should suppress flushing for a read-only transaction.
if (!flushMode.equals(FlushMode.MANUAL)) {
session.setFlushMode(FlushMode.MANUAL);
return flushMode;
}
}
// ...
}
Transaction의 설정이 readOnly = true라면 Hibernate Session의 FlushMode를 MANUAL(명시적으로 EntityManager.flush() 메서드를 호출하기 전까지 flush 되지 않음)로 강제하고 있다.
그리고 나서 실질적인 로직이 끝난 이후에 TransactionAspectSupport.invokeWithinTransaction() 메서드에서 아래와 같은 호출 흐름을 가진다.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
protected Object invokeWithinTransaction(Method method, Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {
// ...
if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
// Standard transaction demarcation with getTransaction and commit/rollback calls.
TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
Object retVal;
try {
// This is an around advice: Invoke the next interceptor in the chain.
// This will normally result in a target object being invoked.
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// target invocation exception
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}
if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {
// Set rollback-only in case of Vavr failure matching our rollback rules...
TransactionStatus status = txInfo.getTransactionStatus();
if (status != null && txAttr != null) {
retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
}
}
commitTransactionAfterReturning(txInfo);
return retVal;
}
// ...
}
- invocation.proceedWithInvocation()에 의해 트랜잭션 내부 로직을 호출한다.
- TransactionAspectSupport.commitTransactionAfterReturning()
- AbstractPlatformTransactionManager.commit()
- AbstractPlatformTransactionManager.processCommit()
- JpaTransactionManager.doCommit()
- TransactionImpl.commit()
- JdbcResourceLocalTransactionCoordinatorImpl.TransactionDriverControlImpl.commit()
- JdbcResourceLocalTransactionCoordinatorImpl.beforeCompletionCallback()
- JdbcCoordinatorImpl.beforeTransactionCompletion()
- SessionImpl.beforeTransactionCompletion()
- SessionImpl.flushBeforeTransactionCompletion()
SessionImpl.flushBeforeTransactionCompletion 코드를 보면 아래와 같다.1
2
3
4
5
6
7
8
9
10
11
12
13public void flushBeforeTransactionCompletion() {
final boolean doFlush = isTransactionFlushable()
&& getHibernateFlushMode() != FlushMode.MANUAL;
try {
if ( doFlush ) {
managedFlush();
}
}
catch (RuntimeException re) {
throw ExceptionMapperStandardImpl.INSTANCE.mapManagedFlushFailure( "error during managed flush", re, this );
}
}
위에서 readOnly이면 Hibernate Session의 Flush 모드를 MANUAL로 강제했기 때문에 getHibernateFlushMode()는 MANUAL이 나오기 때문에getHibernateFlushMode() != FlushMode.MANUAL
는 false이기 때문에 doFlush는 false라서 managedFlush 메서드를 호출하지 않아서 실질적으로 flush가 호출되지 않는다.
@Transaction(readOnly = true)에 의해 시작된 트랜잭션도 종료를 해야하기 때문에 커밋을 한다.
flush는 하지 않았지만 트랜잭션을 시작했기 때문에 트랜잭션을 종료해야 정상적으로 커넥션을 반환하게 된다.
다시 JdbcResourceLocalTransactionCoordinatorImpl.TransactionDriverControlImpl.commit() 로 돌아오면
위와 같은 메서드 호출을 통해 실제 DB 물리 커넥션에 commit을 날리기 때문에 위에서 시작한 트랜잭션을 종료하게 된다.