스프링으로 개발을 하다 보면 SELECT 쿼리만을 필요로 하는 부분에 @Transactional(readOnly = true) 어노테이션을 사용하는 경우가 많습니다. 그런데 데이터베이스 관점에서 보면 단순히 SELECT 하는 쿼리라면 트랜잭션이 필요 없습니다. 그렇다면 @Transactional(readOnly = true)는 왜 사용하는 걸까요? 그냥 해당 어노테이션 없이 사용해도 되지 않을까요?

@Transactional 이해하기

우선 @Transactional에 대해 간단히 설명드리겠습니다.

@Transactional은 스프링 프레임워크에서 제공하는 선언적 트랜잭션 관리를 위한 어노테이션입니다. 이 어노테이션을 메서드나 클래스에 적용하면, 해당 메서드 또는 클래스의 모든 public 메서드가 트랜잭션 내에서 실행됩니다.
(claude)

@Transactional은 어노테이션 기반으로 트랜잭션 관리를 쉽게 도와줍니다. 이 어노테이션은 프록시를 기반으로 동작하며, 주요 특징은 다음과 같습니다:

  1. 프록시 생성: 스프링은 @Transactional이 적용된 빈에 대해 프록시를 생성합니다. 이 프록시는 원본 객체를 감싸고 있으며, 메서드 호출을 가로채서 트랜잭션 로직을 추가합니다.
  2. 트랜잭션 매니저: 프록시는 PlatformTransactionManager를 사용하여 트랜잭션을 관리합니다. 이 매니저는 트랜잭션의 시작, 커밋, 롤백을 담당합니다.
  3. AOP (Aspect-Oriented Programming): @Transactional의 동작은 AOP를 기반으로 합니다. 트랜잭션 관리는 횡단 관심사로 처리되며, 이를 통해 비즈니스 로직과 트랜잭션 처리 로직을 분리할 수 있습니다.
  4. 트랜잭션 전파: @Transactional은 다양한 전파 옵션을 가지고 있습니다. 기본값은 REQUIRED로, 이미 진행 중인 트랜잭션이 있으면 그 트랜잭션에 참여하고, 없으면 새로운 트랜잭션을 시작합니다.
  5. 예외 처리: 기본적으로 런타임 예외가 발생하면 트랜잭션이 롤백됩니다. 체크 예외는 롤백되지 않지만, rollbackFor 속성을 사용하여 롤백 동작을 커스터마이즈할 수 있습니다.
  6. 트랜잭션 동기화: 스프링은 ThreadLocal을 사용하여 트랜잭션 컨텍스트를 관리합니다. 이를 통해 동일한 스레드 내에서 여러 데이터 액세스 작업을 하나의 트랜잭션으로 묶을 수 있습니다.

 

(출처:

https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/tx-decl-explained.html

)

readOnly 옵션의 역할

이제 readOnly 옵션에 대해 자세히 살펴보겠습니다.

This just serves as a hint for the actual transaction subsystem; it will not necessarily cause failure of write access attempts. A transaction manager which cannot interpret the read-only hint will not throw an exception when asked for a read-only transaction.
(Javadoc)

readOnly 옵션은 실제로 데이터베이스에 대한 쓰기를 물리적으로 방지하지는 않습니다. 하지만 힌트를 제공함으로써 데이터베이스 수준의 최적화를 할 수 있습니다. 특히 Hibernate에서는 최적화 효과가 있습니다.

readOnly 옵션이 설정된 트랜잭션에서 Hibernate는 영속성 컨텍스트의 동작을 크게 최적화합니다:

  • 더티 체킹의 비활성화: 엔티티의 변경사항을 추적하지 않음으로써 성능이 향상됩니다.
  • 엔티티의 초기 상태 스냅샷 관리 최소화: 메모리 사용량이 감소합니다.
  • FlushMode를 MANUAL로 설정: 불필요한 데이터베이스 동기화가 방지됩니다.
  • 2차 캐시에 엔티티를 저장하지 않음: 캐시 오염을 막을 수 있습니다.
  • 쓰기 지연 저장소 비활성화: 추가적인 메모리 최적화가 이루어집니다.

그러나 이러한 최적화 효과를 얻기 위해서는 주의할 점이 있습니다. readOnly 옵션은 Propagation.REQUIRED 또는 REQUIRES_NEW일 때만 효과적으로 동작합니다.

읽기 작업에서 @Transactional(readOnly = true)의 필요성

데이터베이스 관점에서는 순수한 읽기 작업에 대해 트랜잭션을 사용하는 것은 일반적으로 오버헤드를 증가시킵니다. 그러나 일부 상황에서는 읽기 작업에도 트랜잭션이 필요할 수 있습니다:

  • 특정 시점의 일관된 데이터 스냅샷이 필요한 경우
  • 특정 격리 수준을 보장해야 하는 경우

또한 Hibernate와 같은 ORM을 사용할 때 readOnly 옵션은 앞서 설명한 최적화를 제공합니다. 이는 특히 대용량 데이터를 읽을 때 유용할 수 있습니다. MySQL의 InnoDB 엔진은 readOnly 트랜잭션에 대해 특정 최적화를 수행해주기도 합니다.

결론

단순 조회 쿼리에도 @Transactional(readOnly = true)를 사용하는 것이 여러 이점이 있습니다. 그러나 최종적으로는 항상 성능 테스트를 통해 결정을 내리는 것이 좋습니다.

참고 자료

+ Recent posts