1) 한 줄 정의
@Transactional(readOnly = true)는 해당 트랜잭션이 읽기 전용임을 트랜잭션 관리자에게 힌트로 제공해,
런타임에 알맞은 최적화를 가능하게 하는 설정이다.
이 값은 기본값이 아니며, 명시하지 않으면 읽기/쓰기 트랜잭션으로 동작한다.
이 설정은 어디까지나 힌트이므로, 구현체가 이를 해석하지 못하면 무시될 수 있다.
2) 무엇이 최적화되는가
(1) JPA/Hibernate 관점
Spring + Hibernate 조합에서 읽기 전용 트랜잭션을 사용하면 일반적으로 세션 Flush 모드가 MANUAL로 바뀌고,
기본 읽기 전용 플래그를 활용하여 Dirty Checking(변경 감지)을 최소화한다.
그 결과 스냅샷 보관이 줄거나 생략되어 메모리/CPU 사용량이 감소한다.
Flush가 자동으로 발생하지 않으므로, 강제로 flush()하지 않는 한 DB에 쓰기 반영이 이뤄지지 않는다.
(2) JDBC/DB 관점
트랜잭션이 읽기 전용 커넥션으로 힌트가 전달될 수 있으며(DB 드라이버와 데이터베이스 지원에 따름),
쿼리 실행 계획이나 캐시 전략에서 읽기 전용에 적합한 경로가 선택될 여지가 생긴다.
다만 구현체/드라이버에 따라 효과가 상이할 수 있다.
3) 왜 써야 하는가: 세 가지 이점
- 성능 최적화
변경 감지와 스냅샷 보관이 줄어 읽기 전용 조회의 처리 효율이 높아진다.
대량 조회, 보고서성 쿼리, 목록 API에서 효과가 크다. - 안전성 향상(개발 의도 명시)
해당 메서드가 “쓰기 금지” 의도임을 코드에 새겨 실수로 엔티티를 수정하는 상황을 줄인다. - 불필요한 flush 억제
커밋 직전 자동 flush가 이뤄지지 않아, 우발적 변경 반영을 사전에 차단한다.
4) 주의할 점
- 쓰기 로직을 섞지 말아야 한다
읽기 전용 트랜잭션 안에서 엔티티를 수정하면, 구현체에 따라 예외가 나지 않고 변경이 무시되거나 충돌 감지가 비정상이 될 수 있다. 특히 낙관적 락(@Version)을 사용하는 경우, readOnly 트랜잭션에서 수정이 섞이면 버전 검증·충돌 감지가 의도대로 동작하지 않을 수 있다. 읽기 메서드에는 진짜로 읽기만 두는 것이 원칙이다. - 힌트일 뿐 절대 규칙이 아니다
Spring 공식 문서에서 readOnly는 “트랜잭션 서브시스템을 위한 힌트”라고 못 박는다.
일부 스토리지/드라이버/ORM에서는 효과가 제한적일 수 있으며,
쓰기 시도가 즉시 실패하지 않을 수도 있다.
행위 보장은 애플리케이션 레벨 규율로 보완해야 한다. - 미세한 오버헤드가 생길 수도 있다
특정 조합에서는 세션/커넥션의 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(); // 쓰기 로직은 명시적으로 전환
}
}
- 기본을 읽기로 두고, 정말 필요한 쓰기 메서드만 별도로 쓰기 트랜잭션을 건다.
반응형
'IT > Spring' 카테고리의 다른 글
| CompletableFuture + Spring @Async (0) | 2026.01.02 |
|---|---|
| JUnit의 테스트 생명주기 어노테이션 (0) | 2025.11.30 |
| H2 인메모리 DB의 동작 원리 (2) | 2025.08.12 |
| Spring Boot에서 @Valid와 @ExceptionHandler로 유효성 검사 에러 처리 (2) | 2025.08.05 |
| Spring Data JPA 페이지네이션(Pagination) 처리 (1) | 2025.07.30 |