실무에서 @Transactional을 제거했더니 성능이 2배 향상된 이유

2025. 5. 8. 22:38·Spring/Hibernate

Spring에서 @Transactional(readOnly = true)는 DirtyChecking 모드를 Manual 모드로 바꿔줘 성능 최적화를 위해 사용된다고 알려져 있지만, 오히려 불필요한 JDBC 호출로 인해 성능이 저하될 수 있다. 이 글은 Elastic APM을 통해 확인한 실제 호출 로그를 바탕으로 readOnly 트랜잭션이 성능에 어떤 영향을 주는지 분석하고, 실무적으로 더 나은 대안을 제시한다.

 

아는 사람만 아는 @Transactional의 비밀

다음 API는 @Transactional(readOnly = true)로 선언된 Service 메소드를 호출한다. Elastic APM을 통해 추적한 결과, 해당 API는 단 2개의 SELECT 쿼리만 실행함에도 불구하고, 여러 번 JDBC 호출이 발생하는 것을 확인했다.

@Transactional 위해 호출되는 여러 JDBC 기능들을 Elastic APM으로 확인
쿼리는 분명 2개만 실행했지만 여러 기능들이 호출되는 것을 확인할 수 있다.

그렇다면 과연 어떤 동작들이 포함되어 있는 것일까?

 

set_option

이는 JPA에서 @Transactional을 사용하면서 트랜잭션을 관리하기 위해 내부적으로 실행되는 설정(set_option) 때문이다. 이 과정에서 다음과 같은 JDBC 호출이 자동으로 발생하며, 이들은 성능에도 영향을 줄 수 있다.

1. Set transaction access mode 'read-only'

@Transactional(readOnly = true)는 JDBC 레벨에서 Connection을 read-only로 설정하도록 만든다. 실제 구현 레벨에서는 아래와 같은 코드를 호출한다.

Connection.setReadOnly(true)

이 설정은 MySQL, PostgreSQL 등 일부 DB에서 실행 계획 최적화에 도움이 될 수 있지만 대부분의 JDBC 드라이버는 이 설정이 필수는 아니며, 추가적인 네트워크 왕복을 유발할 수도 있다.

 

2. autocommit

Spring에서는 트랜잭션을 시작할 때 JPA의 FlushMode도 함께 설정된다. 기본값이 FlushModeType.AUTO로 지정되어 있으며, 이 설정은 트랜잭션 commit 시점이나 JPQL 실행 시 flush를 자동으로 발생시키는 설정이다.

public interface Session extends SharedSessionContract, EntityManager {

	/**
	 * Set the current {@link FlushModeType JPA flush mode} for this session.
	 * <p>
	 * <em>Flushing</em> is the process of synchronizing the underlying persistent
	 * store with persistable state held in memory. The current flush mode determines
	 * when the session is automatically flushed.
	 *
	 * @param flushMode the new {@link FlushModeType}
	 *
	 * @see #setHibernateFlushMode(FlushMode) for additional options
	 */
	@Override
	void setFlushMode(FlushModeType flushMode);
    
}
Session.setHibernateFlushMode(FlushMode.AUTO)

3. Rollback

일반적으로 Rollback은 예외 발생 시 실행된다고 생각하기 쉽지만, DB 트랜잭션은 명시적인 commit 또는 rollback으로만 종료되기 때문에 예외가 발생하지 않아도 Rollback을 호출할 수도 있다. 예를 들어, @Transactional(readOnly = true)이 적용된 경우, Spring은 변경사항이 없다고 판단하고 명시적으로 rollback()을 호출하여 트랜잭션을 종료한다.

이는 실제 DB에는 아무런 변경이 없더라도, JDBC 드라이버 수준에서 rollback 요청이 발생한다는 의미이며, 역시 불필요한 비용이 될 수 있다.

 

 

@Transactional 제거 시의 변화

앞서 소개한 API에서 @Transactional(readOnly = true)을 제거하고 실행해봤다. Set transaction access mode 'read-only', autocommit, Rollback 등의 호출이 발생하지 않게 되었다. 그 결과, API 응답 속도가 절반 가까이 줄어든 것을 확인할 수 있었다.

@Transactional을 제거했더니 속도가 2배가 되었다.
@Transactional을 뺀 것만으로도 속도가 빨라진다.

 

흔히 @Transactional(readOnly = true)가 dirtyChecking 모드를 Manual모드로 바꿔 줄 수 있기 때문에 application 성능 개선에 도움이 된다고 알려져 있지만, 실제로는 아예 트랜잭션 자체를 제거하는 것이 오히려 더 큰 성능 향상을 가져왔다.

 

Replication 환경에서는 주의!

단순히 성능을 위해 @Transactional을 제거하는 접근은 DB Replication을 환경에서는 문제가 될 수 있다. DB의 처리량을 증가시키고, 고가용성을 확보하기 위해 Replication을 구성한 시스템에서는 보통 AbstractRoutingDataSource을 사용하여 트랜잭션이 읽기 전용인지 여부를 따져 Primary 또는 Replica DB로 요청을 분기한다.

final AbstractRoutingDataSource dataSourceRouter =
        new AbstractRoutingDataSource() {
          @Override
          protected Object determineCurrentLookupKey() {
            return TransactionSynchronizationManager.isCurrentTransactionReadOnly()
                ? REPLICA_DATASOURCE_KEY
                : PRIMARY_DATASOURCE_KEY;
          }
        };

 

이처럼 TransactionSynchronizationManager.isCurrentTransactionReadOnly()를 통해 분기를 처리하기 때문에, 트랜잭션 자체를 제거하면 읽기 요청이 모두 Master로 몰리는 부작용이 발생할 수 있다. 이는 Replica 부하 분산이 깨지고, 전체 시스템 성능에 악영향을 줄 수 있다.

 

이러한 문제를 해결하기 위해, 카카오페이 테크 블로그에서는 다음과 같은 형태의 커스텀 어노테이션을 제안했다.

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public @interface ReadOnlyTransactional {}

 

SUPPORTS 전파 수준은 메서드가 트랜잭션 없이 단독으로 호출되면 트랜잭션을 생성하지 않으며, 상위에 트랜잭션이 있을 경우에는 그 트랜잭션에 참여한다. 

즉, @ReadOnlyTransactional만 적용된 메소드에서는 JPA 트랜잭션이 시작되지 않아 setReadOnly, rollback 등 불필요한 JDBC 호출을 피할 수 있고, 동시에 AbstractRoutingDataSource 기반의 Replica 라우팅 로직도 정상 동작하게 된다.

 

반면, 상위 계층에서 이미 @Transactional(REQUIRED) 트랜잭션이 시작된 경우에는 @ReadOnlyTransactional은 영향을 주지 않기 때문에, 기존 트랜잭션 구조에 유연하게 섞어 사용할 수 있는 장점도 있다.

 

Lazy Loading을 사용할 때도 주의!

만약 API에서 Lazy Loading을 사용하고 있는데, @Transactional(readOnly = true)를 제거한다면 무슨 일이 벌어질까? API를 호출할 때마다 아래 에러가 발생할 것이다.

org.hibernate.LazyInitializationException: could not initialize proxy - no Session

 

Spring에서 트랜잭션을 시작하지 않으면 JPA는 영속성 컨텍스트를 사용할 수 없기 때문에 Lazy Loading 또한 사용할 수 없게 된다. 기술적인 이유로 어쩔 수 없이 Lazy Loading을 사용해야 하는 API라면 @Transactional을 붙여 영속성 컨텍스트를 사용할 수 있는 환경을 만들어야 한다.

 

 

결론

@Transactional(readOnly = true)는 흔히 읽기 성능 최적화를 위해 사용하는 설정이라고 알려져 있지만, 실제로는 JDBC 수준에서 Set readOnly, AutoCommit, Rollback 등 불필요한 호출이 추가적으로 발생할 수 있다는 사실을 파악했다.

 

또한 @ReadOnlyTransactional과 같이 propagation을 SUPPORTS로 설정한 어노테이션을 도입하는 방법을 제시하여 트랜잭션의 오버헤드는 줄이되, Replica 라우팅 로직은 유지하는 방법을 알아봤다.

 

여기서 중요한 것은, 서비스의 트랜잭션 전략을 일률적으로 가져가지 않고, API의 목적과 시스템 구조에 따라 유연하게 적용하는 것이다. 불필요한 @Transactional 사용이 오히려 성능 병목이 될 수 있다는 점을 기억하고, 필요에 따라 readOnly 분리 전략을 고민해보는 것이 실무에서의 핵심이다.

 

 

참고자료

  • JPA Transactional 잘 알고 쓰고 계신가요?, 카카오 pay tech 블로그
  • Spring @Transactional read-only mode rollback behavior, stack overflow

 

'Spring/Hibernate' 카테고리의 다른 글
  • [오류해결] java.lang.NullPointerException - 스프링 & 하이버네이트
  • [오류해결] org.hibernate.hql.internal.ast.QuerySyntaxException
  • [Spring] 하이버네이트(Hibernate) 사용하는법
gakko
gakko
좌충우돌 개발기
  • gakko
    MYVELOP 마이벨롭
    gakko
  • 전체
    오늘
    어제
    • 분류 전체보기 (203)
      • Spring (23)
        • Spring (10)
        • Spring Boot (7)
        • Spring Security (1)
        • Hibernate (4)
      • Test (3)
      • 끄적끄적 (6)
      • 활동 (35)
        • 부스트캠프 (23)
        • 동아리 (3)
        • 컨퍼런스 (3)
        • 글또 (5)
        • 오픈소스 컨트리뷰션 (1)
      • 디자인패턴 (0)
      • Git & GitHub (22)
        • Git (13)
        • Github Actions (1)
        • 오류해결 (5)
        • 기타(마크다운 등) (3)
      • 리눅스 (6)
        • 기초 (6)
        • 리눅스 서버 구축하기 (0)
      • Infra (2)
        • Docker (1)
        • Elastic Search (0)
        • Jenkins (1)
        • AWS (1)
      • MySQL (7)
        • 기초 (6)
        • Real MySQL (1)
      • 후기 (3)
        • Udemy 리뷰 (3)
      • CS (26)
        • 웹 기본지식 (0)
        • 자료구조 (13)
        • 운영체제 OS (12)
        • 데이터베이스 (1)
        • 시스템 프로그래밍 (0)
        • 기타 (0)
      • Tools (1)
        • 이클립스 (1)
        • IntelliJ (0)
      • 프로젝트 (1)
        • 모여모여(부스트캠프) (1)
      • JAVA (32)
        • Maven (6)
        • 오류해결 (11)
        • 자바 클래스&메소드 (1)
        • JSP & Servlet (12)
      • Javascript (5)
        • 기초 (3)
        • React (2)
      • Python (28)
        • 파이썬 함수 (9)
        • 알고리즘 문제풀이 (16)
        • 데이터 사이언스 (2)
        • 웹 크롤링 (1)
      • 단순정보전달글 저장소 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • 우진님
  • 공지사항

  • 인기 글

  • 태그

    부스트캠프 멤버십
    자바
    java
    MySQL
    웹개발
    스프링
    오류해결
    운영체제
    jsp
    Git
    Spring
    스프링부트
    알고리즘
    부스트캠프
    GitHub
    파이썬
    Python
    자바스크립트
    os
    부스트캠프 7기
  • 최근 댓글

  • hELLO· Designed By정상우.v4.10.0
gakko
실무에서 @Transactional을 제거했더니 성능이 2배 향상된 이유
상단으로

티스토리툴바