IT/Spring

@Transactional(readOnly = true)

iamhyeon 2025. 11. 11. 20:22

1) 한 줄 정의

@Transactional(readOnly = true)는 해당 트랜잭션이 읽기 전용임을 트랜잭션 관리자에게 힌트로 제공해,

런타임에 알맞은 최적화를 가능하게 하는 설정이다.

이 값은 기본값이 아니며, 명시하지 않으면 읽기/쓰기 트랜잭션으로 동작한다.

이 설정은 어디까지나 힌트이므로, 구현체가 이를 해석하지 못하면 무시될 수 있다. 

 

2) 무엇이 최적화되는가

(1) JPA/Hibernate 관점

Spring + Hibernate 조합에서 읽기 전용 트랜잭션을 사용하면 일반적으로 세션 Flush 모드가 MANUAL로 바뀌고,

기본 읽기 전용 플래그를 활용하여 Dirty Checking(변경 감지)을 최소화한다.

그 결과 스냅샷 보관이 줄거나 생략되어 메모리/CPU 사용량이 감소한다.

Flush가 자동으로 발생하지 않으므로, 강제로 flush()하지 않는 한 DB에 쓰기 반영이 이뤄지지 않는다.

(2) JDBC/DB 관점

트랜잭션이 읽기 전용 커넥션으로 힌트가 전달될 수 있으며(DB 드라이버와 데이터베이스 지원에 따름),

쿼리 실행 계획이나 캐시 전략에서 읽기 전용에 적합한 경로가 선택될 여지가 생긴다.

다만 구현체/드라이버에 따라 효과가 상이할 수 있다.

 

3) 왜 써야 하는가: 세 가지 이점

  1. 성능 최적화
    변경 감지와 스냅샷 보관이 줄어 읽기 전용 조회의 처리 효율이 높아진다.
    대량 조회, 보고서성 쿼리, 목록 API에서 효과가 크다.
  2. 안전성 향상(개발 의도 명시)
    해당 메서드가 “쓰기 금지” 의도임을 코드에 새겨 실수로 엔티티를 수정하는 상황을 줄인다.
  3. 불필요한 flush 억제
    커밋 직전 자동 flush가 이뤄지지 않아, 우발적 변경 반영을 사전에 차단한다. 


4) 주의할 점

  1. 쓰기 로직을 섞지 말아야 한다
    읽기 전용 트랜잭션 안에서 엔티티를 수정하면, 구현체에 따라 예외가 나지 않고 변경이 무시되거나 충돌 감지가 비정상이 될 수 있다. 특히 낙관적 락(@Version)을 사용하는 경우, readOnly 트랜잭션에서 수정이 섞이면 버전 검증·충돌 감지가 의도대로 동작하지 않을 수 있다. 읽기 메서드에는 진짜로 읽기만 두는 것이 원칙이다. 
  2. 힌트일 뿐 절대 규칙이 아니다
    Spring 공식 문서에서 readOnly는 “트랜잭션 서브시스템을 위한 힌트”라고 못 박는다.
    일부 스토리지/드라이버/ORM에서는 효과가 제한적일 수 있으며,
    쓰기 시도가 즉시 실패하지 않을 수도 있다.
    행위 보장은 애플리케이션 레벨 규율로 보완해야 한다.
  3. 미세한 오버헤드가 생길 수도 있다
    특정 조합에서는 세션/커넥션의 read-only 전환 호출 때문에 왕복 비용이 늘어나는 사례도 보고된다.
    신중히 프로파일링하고, 일괄적으로 전역 적용하기보다 읽기 트래픽이 많은 경로에 선별 적용하는 것이 합리적이다. 

 

5) 언제 쓰면 좋은가

  • 조회 전용 서비스 메서드: 목록/상세 조회, 검색, 통계, 리포트
  • CQRS 분리된 Query 단: Query 전용 UseCase/리포지토리 계층
  • 대량 읽기 시나리오: 페이지네이션, 배치 리더 단계(쓰기 없는 구간)

 

6) 실제 적용 패턴

(1) 서비스 계층에서 메서드 단위 적용

@Service
@RequiredArgsConstructor
public class PostQueryService {
    private final PostRepository postRepository;

    @Transactional(readOnly = true)
    public PostDto findPost(Long id) {
        return postRepository.findById(id)
                .map(PostDto::from)
                .orElseThrow(NotFoundException::new);
    }
}
  • 쓰기 위험이 섞이는 순간(로그 적재, 조회수 증가 등) 별도 쓰기 서비스기능 분리가 안전하다.

(2) 클래스 레벨 + 예외적 쓰기 메서드 분리

@Service
@Transactional(readOnly = true)  // 기본은 읽기 전용
public class PostReadFacade {

    private final PostRepository postRepository;

    public List<Post> list() { return postRepository.findAll(); }

    @Transactional // 필요한 메서드만 쓰기 허용
    public void touchAudit(Long id) {
        var p = postRepository.getReferenceById(id);
        p.touch(); // 쓰기 로직은 명시적으로 전환
    }
}
  • 기본을 읽기로 두고, 정말 필요한 쓰기 메서드만 별도로 쓰기 트랜잭션을 건다.

 

반응형