개요

Spring AOP 방식의 트랜잭션을 지원하는 파라미터 중 readOnly에 대해서 정리합니다. 

 

 

발단

JPA에서 영속성 관리로 인하여 readOnly 트랜잭션을 사용했을 때 성능 향상이 있다고 말하고, 많은 개발자들이 인지하고 있는 부분입니다. 
정확하게 Spring, JPA hibernate 스택에서 어떤 부분에 영향을 미쳐서 프로그램 상 이득이 있는 것일까요?

 

조사

우선 readOnly를 설정하게 되면 다음과 같은 과정이 일어납니다.

  1.  Jdbc의 Connection.setReadOnly를 호출 하며, 이 때 각각 트랜잭션이 생성되는 것은 각 jdbc 드라이버에 따라 다름.
  2.  hibernate 환경에서 적용 시, FlushMode를 변경함.


2번을 확인하기 위해 Junit 테스트 코드를 작성하였습니다. 

> @Transactional 일 때 Session 의 상태 

    @Test
    @Transactional
    @Rollback(value = false)
    public void testTransactional1() {
        Product product = productRepository.findOne(2001611253L);
        product.setSalePrice(BigDecimal.valueOf(3000));
    }




> @Transactional(readOnly=true) 일 때 Session의 상태

    @Test
    @Transactional(readOnly=true)
    @Rollback(value = false)
    public void testTransactional2() {
        Product product = productRepository.findOne(2001611253L);

        product.setSalePrice(BigDecimal.valueOf(4000));
    }


FlushMode가 readOnly Attribute에 따라 다르게 적용되는 것을 코드로 확인하였습니다. 


여기서 FlushMode란, EntityManager가 관리하고 있는 객체를 데이터베이스와 sync하는 과정입니다.


참고 >

entity life cycle

flushMode의 타입에 따라 JPA의 변경 감지 반영 동작이 변경되게 됩니다. 
그 부분은 hibernate의 다음과 같은 코드에서 찾을 수 있는데요. 

	private void dirty(PersistentCollection collection) throws HibernateException {

		boolean forceDirty = collection.wasInitialized() &&
				!collection.isDirty() && //optimization
				getLoadedPersister() != null &&
				getLoadedPersister().isMutable() && //optimization
				( collection.isDirectlyAccessible() || getLoadedPersister().getElementType().isMutable() ) && //optimization
				!collection.equalsSnapshot( getLoadedPersister() );

		if ( forceDirty ) {
			collection.dirty();
		}

	}

코드의 네이밍에서 유추 할 수 있듯이, 객체가 변경되었는지 체크하는 모습입니다. (이 코드는 hibernate-core:5.0.11.FINAL 기준입니다.) 
위 코드가 호출되는 스택은 이렇습니다. 

  1. SessionImpl.flush()
  2. AbstractFlushingEventListener.flushEverythingToExecutions
  3. AbstractFlushingEventListener.prepareCollectionFlushes
  4. CollectionEntry.preFlush
  5. CollectionEntry.dirty

flush 과정에서, dirty 체크가 들어가는 것을 콜 스택으로 알 수 있습니다.


그렇다면, FlushMode가 위 과정에 어떤 영향을 미치는 것일까요? 
@Transactional 호출이 끝난 후 호출되는 메서드를 디버깅 해보았습니다. 

트랜잭션 완료 후 SessionImpl.beforeTransactionCompletion 을 호출합니다.

	@Override
	public void beforeTransactionCompletion() {
		LOG.tracef( "SessionImpl#beforeTransactionCompletion()" );
		flushBeforeTransactionCompletion();
		actionQueue.beforeTransactionCompletion();
		try {
			interceptor.beforeTransactionCompletion( currentHibernateTransaction );
		}
		catch (Throwable t) {
			LOG.exceptionInBeforeTransactionCompletionInterceptor( t );
		}
	}
이 코드에서 flushBeforeTransactionCompletion을 호출하게 되는데요,
여기서 flushMode가 판단 기준으로 사용됩니다. (managedFlushChecker.shouldDoManagedFlush)

	@Override
	public void flushBeforeTransactionCompletion() {
		boolean flush = isTransactionFlushable() && managedFlushChecker.shouldDoManagedFlush( this );
		try {
			if ( flush ) {
				managedFlush();
			}
		}
		catch (HibernateException he) {
			throw exceptionMapper.mapManagedFlushFailure( "error during managed flush", he );
		}
		catch (RuntimeException re) {
			throw exceptionMapper.mapManagedFlushFailure( "error during managed flush", re );
		}
	}

따라서 FlushMode가 MANUEL 일 경우, isTransactionFlushable() = false 가 되고, flush가 수행되지 않으며 
이는 스냅샷 & 더티 체킹을 줄여주는 효과로 성능 향상이 있다고 확인 할 수 있었습니다. 

명시적 방식의 트랜잭션


AOP 방식으로 트랜잭션을 적용할 수 있지만, 명시적으로 적용할 필요가 있을 떄가 있는데요. 
이 때 TransactionalTemplate를 사용 할 수 있습니다.  이 객체를 사용했을 때 @Transactional에 readOnly를 설정했던 것처럼 하려면 
setReadOnly() 메서드를 호출해주면 됩니다. 

        TransactionTemplate transactionTemplate = new TransactionTemplate(serviceTm());
        transactionTemplate.setReadOnly(true);


다만, 이 속성은 TransactionTemplate에 영구히 적용되므로, Spring Singleton 타입에 적용하기에 Risk가 큽니다. 
따라서 별도의 readOnlyTemplate를 만들어 각각의 경우에 맞게 사용하면 됩니다. 

    @Bean
    @Qualifier("transactionReadOnlyTemplate")
    public TransactionTemplate transactionReadOnlyTemplate() {
        TransactionTemplate transactionTemplate = new TransactionTemplate(serviceTm());
        transactionTemplate.setReadOnly(true);
        return transactionTemplate;
    }



번외

아울러 readOnly 타입에 따라 RoutingDataBase로 사용하여, read DB, write DB로 커넥션을 맺는 구조를 사용하기도 합니다. 

https://vladmihalcea.com/read-write-read-only-transaction-routing-spring/

+ Recent posts