
팀프로젝트중에 service 부분을 개발하면서 @Transactional에 관해서 공부가 필요해서 이렇게 정리해본다.
트랜젝션 동작 순서
Spring에서 @Transactional은 하나의 트랜잭션 단위에서 DB 작업을 안정적으로 처리하기 위한 핵심 어노테이션이다.
그 중에서도 @Transactional(readOnly = true)는 읽기 전용 조회 성능을 최적화하기 위해 사용된다.
단순히 “쓰기 기능이 없어서 붙인다” 정도로 끝나는 것이 아니라,
JPA/Hibernate 내부 동작이 어떻게 달라지는지 이해해야 왜 필요한지 명확해진다.
효과: readOnly = true가 단순히 “쓰기 없음”뿐만 아니라 성능 최적화에 쓰인다는 것을 보여줌
readOnly = true가 설정되면 Hibernate는 해당 트랜잭션을 쓰기 트랜잭션이 아닌 조회 트랜잭션으로 인식한다.
그 결과 다음과 같은 최적화가 이루어진다.
일반적인 트랜잭션에서는 엔티티가 조회되면 Hibernate는 이를 영속성 컨텍스트에 저장하고,
트랜잭션 종료 시 값이 변경되었는지 비교(Dirty Checking) 한 뒤 변경 시점에 UPDATE 쿼리를 자동 실행한다.
하지만 조회 전용에서 Dirty Checking은 불필요한 CPU 연산과 비용이다.
readOnly = true 효과
- Hibernate가 Dirty Checking 수행 X
- 엔티티 변경 여부를 추적하지 않으므로 불필요한 계산 감소
- 퍼포먼스 개선
한 마디로: “조회만 할 거니까 변경 체크는 하지 말자”
→ 조회 성능이 더 빨라진다.
일반 트랜잭션은 COMMIT 시점에 변경사항 반영을 위해 Flush를 발생시키지만
읽기 전용 트랜잭션에서는 Flush 과정 자체를 생략한다.
Flush 비활성화 효과
- DB와의 불필요한 동기화를 수행하지 않음
- 트랜잭션 종료 시 추가 쿼리 발생 X
- 전체적인 수행 비용 절감
일부 DB는 read-only 세션을 최적화하여
등의 효과가 있다.
(DB별로 동작은 상이하지만, 최소한 불필요한 쓰기 락을 줄여 효율적이다.)
@Transactional(readOnly = true)를 붙이면 코드를 읽는 사람에게 의도를 명확히 전달할 수 있다.
즉, 코드의 가독성과 유지보수성도 향상된다.
Lazy Loading 예시
특히 성능이 중요한 서비스에서 다음과 같은 메서드에 사용하면 효과가 크다.
비즈니스에서 가장 많이 호출되는 형태의 API가 대부분 조회 API이기 때문에
readOnly 적용은 전체 트래픽 최적화에 매우 효과적이다.
예시 코드
readOnly = true는 변경 감지를 비활성화하기 때문에 아래 작업에서는 사용하면 안 된다.
조회 전용이 아닌 서비스에서 사용하면
변경이 DB에 반영되지 않는 심각한 오류가 발생할 수 있다.
사용하면 안 되는 코드 예시
| 동작 | 일반 트랜잭션 | readOnly 트랜잭션 |
|---|---|---|
| Dirty Checking | O | ❌ |
| Flush | O | ❌ |
| DB 쓰기 락 가능성 | O | 감소 |
| 엔티티 변경 반영 | O | ❌ |
| 조회 성능 | 기본 | 향상 |
@Transactional(readOnly = true)는 단순한 설정이 아니라
조회 성능 최적화를 위한 Spring/JPA의 강력한 기능이다.
✔ 변경 감지 끔
✔ Flush 끔
✔ 불필요한 리소스 최소화
✔ DB 접근 최적화
✔ 코드 의도 명확화
즉, “읽기 API에는 반드시 readOnly를 붙이는 것이 좋은 습관이다.”
🔹 오늘의 나는 무엇을 잘했는지
오늘은 @Transactional(readOnly = true)의 개념과 내부 동작을 정리하면서, Hibernate Dirty Checking과 Flush 동작이 실제로 어떻게 바뀌는지까지 이해했다.
처음에는 단순히 “조회 전용 트랜잭션”이라고만 알고 있었는데, 내부 최적화 구조까지 확인하며 왜 readOnly가 성능에 도움이 되는지 명확하게 체감할 수 있었다.
작은 예시 코드도 직접 만들어보면서 실제 서비스에서 적용하는 방식까지 연습했다.
🔹 오늘 배운 것 (학습한 내용)