Spring @Transactional readOnly 완벽 가이드
Spring Framework에서 트랜잭션 관리는 애플리케이션 성능과 데이터 일관성에 큰 영향을 미치는 중요한 요소입니다. 그 중에서도 @Transactional
어노테이션의 readOnly
속성은 읽기 전용 트랜잭션을 통해 성능 최적화를 달성할 수 있는 강력한 도구입니다.
1. @Transactional readOnly란?
@Transactional(readOnly = true)
는 해당 메서드나 클래스가 읽기 전용 트랜잭션에서 실행되어야 함을 Spring에게 알려주는 어노테이션입니다. 이는 단순히 개발자에게 힌트를 제공하는 것이 아니라, 실제로 JPA/Hibernate와 데이터베이스 레벨에서 다양한 최적화를 수행합니다.
@Service
@Transactional(readOnly = true)
public class UserService {
@Autowired
private UserRepository userRepository;
// 읽기 전용 메서드
public User findById(Long id) {
return userRepository.findById(id).orElse(null);
}
// 쓰기 작업이 필요한 메서드는 readOnly를 false로 오버라이드
@Transactional(readOnly = false)
public User createUser(User user) {
return userRepository.save(user);
}
}
2. readOnly가 제공하는 최적화 효과
2.1 Hibernate 레벨 최적화
FlushMode 변경: Hibernate는 readOnly 트랜잭션에서 FlushMode를 MANUAL로 설정하여 자동 플러시를 비활성화합니다.
일반적으로 Hibernate는 트랜잭션 커밋 시점이나 쿼리 실행 전에 영속성 컨텍스트의 변경사항을 데이터베이스에 동기화(flush)합니다. 하지만 readOnly 트랜잭션에서는 이러한 플러시 작업이 불필요하므로 생략됩니다.
@Transactional(readOnly = true)
public List<User> findActiveUsers() {
// 이 메서드에서는 flush 작업이 수행되지 않음
return userRepository.findByActiveTrue();
}
2.2 더티 체킹(Dirty Checking) 비활성화
Hibernate는 영속성 컨텍스트에서 관리되는 엔티티의 변경사항을 추적하는 더티 체킹을 수행합니다. readOnly 트랜잭션에서는 이 기능이 비활성화되어 메모리 사용량과 CPU 사용량이 감소합니다.
@Transactional(readOnly = true)
public void processUsers() {
List<User> users = userRepository.findAll();
for (User user : users) {
// 엔티티 변경이 있어도 더티 체킹이 수행되지 않음
user.setLastAccessTime(new Date());
// 실제로는 데이터베이스에 반영되지 않음
}
}
2.3 데이터베이스 레벨 최적화
많은 데이터베이스 시스템에서는 읽기 전용 트랜잭션에 대해 특별한 최적화를 제공합니다:
- 락 최적화: 읽기 전용 트랜잭션에서는 불필요한 락 획득을 피할 수 있습니다
- 커넥션 풀 최적화: 읽기 전용 커넥션 풀을 별도로 관리하여 성능을 향상시킬 수 있습니다
- 복제본 라우팅: 마스터-슬레이브 구조에서 읽기 쿼리를 슬레이브 노드로 라우팅할 수 있습니다
3. 실제 사용 예제
3.1 Repository 레이어에서의 활용
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// 메서드 레벨에서 readOnly 설정
@Query("SELECT u FROM User u WHERE u.email = :email")
@Transactional(readOnly = true)
Optional<User> findByEmail(@Param("email") String email);
// 복잡한 조회 쿼리
@Query("SELECT u FROM User u LEFT JOIN FETCH u.orders WHERE u.active = true")
@Transactional(readOnly = true)
List<User> findActiveUsersWithOrders();
}
3.2 Service 레이어에서의 활용
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Transactional(readOnly = true)
public OrderSummary getOrderSummary(Long userId) {
List<Order> orders = orderRepository.findByUserId(userId);
return OrderSummary.builder()
.totalOrders(orders.size())
.totalAmount(orders.stream()
.mapToDouble(Order::getAmount)
.sum())
.build();
}
@Transactional
public Order createOrder(OrderRequest request) {
Order order = new Order();
order.setUserId(request.getUserId());
order.setAmount(request.getAmount());
return orderRepository.save(order);
}
}
4. 주의사항과 함정
주의! readOnly 트랜잭션에서 데이터 변경 작업을 수행하면 예상과 다른 결과가 발생할 수 있습니다.
4.1 데이터 변경 작업이 무시되는 경우
@Transactional(readOnly = true)
public void updateUserName(Long userId, String newName) {
User user = userRepository.findById(userId).orElseThrow();
user.setName(newName); // 변경은 되지만
userRepository.save(user); // 실제로는 데이터베이스에 저장되지 않음
}
위 코드는 예외를 발생시키지 않지만, 실제로는 데이터베이스에 변경사항이 반영되지 않습니다. 이는 디버깅하기 어려운 버그의 원인이 될 수 있습니다.
4.2 트랜잭션 전파와의 상호작용
@Service
public class UserService {
@Transactional(readOnly = true)
public void processUser(Long userId) {
User user = userRepository.findById(userId).orElseThrow();
// 이 메서드는 새로운 트랜잭션을 시작하므로 정상 동작
updateUserStatus(user);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateUserStatus(User user) {
user.setStatus(UserStatus.PROCESSED);
userRepository.save(user);
}
}
5. 성능 측정 및 모니터링
readOnly 트랜잭션의 효과를 측정하려면 다음과 같은 방법을 사용할 수 있습니다:
@Component
@Slf4j
public class TransactionMonitor {
@EventListener
public void handleTransactionEvent(TransactionEvent event) {
if (event.isReadOnly()) {
log.info("ReadOnly transaction completed: {} ms",
event.getExecutionTime());
}
}
}
6. 베스트 프랙티스
권장사항:
- Service 클래스 레벨에서 readOnly = true로 설정하고, 쓰기 작업이 필요한 메서드에서만 오버라이드
- 조회 전용 Repository 메서드에는 명시적으로 readOnly = true 설정
- 복잡한 보고서 생성이나 통계 조회 메서드에는 반드시 readOnly 사용
- 읽기 전용 트랜잭션에서는 엔티티 변경 작업을 피하고, 필요시 별도 메서드로 분리
7. 결론
Spring의 @Transactional(readOnly = true)
는 단순한 힌트가 아닌 실제 성능 최적화를 제공하는 강력한 도구입니다. 올바르게 사용하면 메모리 사용량 감소, CPU 사용량 감소, 그리고 데이터베이스 레벨에서의 최적화 효과를 얻을 수 있습니다.
하지만 읽기 전용 트랜잭션에서 데이터 변경 작업을 수행할 때의 함정을 이해하고, 적절한 트랜잭션 전파 설정을 통해 예상치 못한 동작을 방지하는 것이 중요합니다. 성능 최적화와 함께 코드의 명확성과 유지보수성을 고려하여 readOnly 속성을 적절히 활용하시기 바랍니다.
이 글이 Spring 트랜잭션 관리에 대한 이해에 도움이 되었기를 바랍니다.