개요
Spring AOP 방식의 트랜잭션을 지원하는 파라미터 중 readOnly에 대해서 정리합니다.
발단
JPA에서 영속성 관리로 인하여 readOnly 트랜잭션을 사용했을 때 성능 향상이 있다고 말하고, 많은 개발자들이 인지하고 있는 부분입니다.
정확하게 Spring, JPA hibernate 스택에서 어떤 부분에 영향을 미쳐서 프로그램 상 이득이 있는 것일까요?
조사
우선 readOnly를 설정하게 되면 다음과 같은 과정이 일어납니다.
- Jdbc의 Connection.setReadOnly를 호출 하며, 이 때 각각 트랜잭션이 생성되는 것은 각 jdbc 드라이버에 따라 다름.
- 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하는 과정입니다.
참고 >

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 기준입니다.)
위 코드가 호출되는 스택은 이렇습니다.
- SessionImpl.flush()
- AbstractFlushingEventListener.flushEverythingToExecutions
- AbstractFlushingEventListener.prepareCollectionFlushes
- CollectionEntry.preFlush
- 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 );
}
}
여기서 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/
'백엔드 > spring' 카테고리의 다른 글
ErrorHandlingDeserializer (0) | 2025.02.11 |
---|---|
@Transactional 과 외부 시스템 연동 (0) | 2024.12.23 |
Deserializer를 직접 구현해서 클래스 패키지 경로를 컨트롤 해보자 (0) | 2022.07.03 |
@InitBinder autoGrowCollectionLimit (0) | 2022.06.04 |