일반적으로 서비스 레벨에서 쓰기 연산(CUD)에 @Transactional 옵션을, 읽기 연산에 @Transactional(readOnly = true)을 사용한다. @Transactional의 readOnly 기본 옵션은 false이다. 만약 쓰기 연산에 @Transactional(readOnly = true)를 사용하면 어떻게 될까? 또한 @Transactional 없이 쓰기 연산을 하면 어떻게 되는지 알아보자.
Spring Data Jpa에서 제공하는 JpaRepository의 기본 구현체는 SimpleJpaRepository
이다. CRD 메서드마다 @Transactional 옵션이 다르니 알아보자.
save 메서드에 @Transactional
이 선언되어있다. 따라서 readOnly = false이다. saveAndFlush, saveAll, saveAllAndFlush도 마찬가지다.
delete 메서드에도 @Transactional
이 선언되어있다. 따라서 readOnly = false이다. deleteAllById, deleteAllByIdInBatch, deleteAll, deleteAllInBatch도 마찬가지다.
findById 메서드에는 @Transactional 어노테이션이 존재하지 않는다. 그러면 어떻게 동작할까?
트랜잭션의 기본 전파 옵션은 Required이므로 findById 메서드는 SimpleJpaRepository의 트랜잭션에 참여하기 때문에 @Transactional(readOnly = true)
의 영향을 받는다. findAll, findAllById도 마찬가지다.
실행환경 : springboot 3.x, H2 database
조건 : open-in-view: false // OSIV 미적용
// 1
@Transactional
public void saveMember1(String name) {
MemberEntity memberEntity = new MemberEntity(name);
memberEntityRepository.save(memberEntity);
System.out.println("1 - Transactional");
}
// 2
@Transactional(readOnly = true)
public void saveMember2(String name) {
MemberEntity memberEntity = new MemberEntity(name);
memberEntityRepository.save(memberEntity);
System.out.println("2 - Transactional");
}
// 3
public void saveMember3(String name) {
MemberEntity memberEntity = new MemberEntity(name);
memberEntityRepository.save(memberEntity);
System.out.println("3 - Transactional 완료");
}
Member를 저장하는 Service에 @Transactional, @Transactional(readOnly = true), @Transactional이 없는 경우 3가지를 실행했다.
// 1
2024-01-26T02:42:26.515+09:00 DEBUG 7919 --- [nio-8080-exec-5] o.s.orm.jpa.JpaTransactionManager : Creating new transaction with name [com.example.demo.MemberEntityService.saveMember1]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2024-01-26T02:42:26.515+09:00 DEBUG 7919 --- [nio-8080-exec-5] o.h.e.t.internal.TransactionImpl : On TransactionImpl creation, JpaCompliance#isJpaTransactionComplianceEnabled == false
2024-01-26T02:42:26.515+09:00 DEBUG 7919 --- [nio-8080-exec-5] o.h.e.t.internal.TransactionImpl : begin
2024-01-26T02:42:26.515+09:00 DEBUG 7919 --- [nio-8080-exec-5] o.s.orm.jpa.JpaTransactionManager : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@467b345e]
2024-01-26T02:42:26.515+09:00 DEBUG 7919 --- [nio-8080-exec-5] o.s.orm.jpa.JpaTransactionManager : Found thread-bound EntityManager [SessionImpl(1111800014<open>)] for JPA transaction
2024-01-26T02:42:26.515+09:00 DEBUG 7919 --- [nio-8080-exec-5] o.s.orm.jpa.JpaTransactionManager : Participating in existing transaction
2024-01-26T02:42:26.515+09:00 DEBUG 7919 --- [nio-8080-exec-5] o.h.j.internal.PersistenceUnitUtilImpl : jakarta.persistence.PersistenceUnitUtil.getIdentifier is only intended to work with enhanced entities (although Hibernate also adapts this support to its proxies); however the passed entity was not enhanced (nor a proxy).. may not be able to read identifier
2024-01-26T02:42:26.516+09:00 DEBUG 7919 --- [nio-8080-exec-5] org.hibernate.engine.spi.ActionQueue : Executing identity-insert immediately
2024-01-26T02:42:26.516+09:00 DEBUG 7919 --- [nio-8080-exec-5] org.hibernate.SQL : insert into member_entity (name,member_id) values (?,default)
2024-01-26T02:42:26.516+09:00 DEBUG 7919 --- [nio-8080-exec-5] o.h.id.IdentifierGeneratorHelper : Natively generated identity (com.example.demo.MemberEntity) : 3
2024-01-26T02:42:26.516+09:00 DEBUG 7919 --- [nio-8080-exec-5] o.h.r.j.i.ResourceRegistryStandardImpl : HHH000387: ResultSet's statement was not registered
2024-01-26T02:42:26.517+09:00 DEBUG 7919 --- [nio-8080-exec-5] o.h.r.j.i.ResourceRegistryStandardImpl : Exception clearing maxRows/queryTimeout [The object is already closed [90007-224]]
1 - Transactional
2024-01-26T02:42:26.517+09:00 DEBUG 7919 --- [nio-8080-exec-5] o.s.orm.jpa.JpaTransactionManager : Initiating transaction commit
2024-01-26T02:42:26.517+09:00 DEBUG 7919 --- [nio-8080-exec-5] o.s.orm.jpa.JpaTransactionManager : Committing JPA transaction on EntityManager [SessionImpl(1111800014<open>)]
2024-01-26T02:42:26.517+09:00 DEBUG 7919 --- [nio-8080-exec-5] o.h.e.t.internal.TransactionImpl : committing
2024-01-26T02:42:26.517+09:00 DEBUG 7919 --- [nio-8080-exec-5] o.h.e.i.AbstractFlushingEventListener : Processing flush-time cascades
2024-01-26T02:42:26.517+09:00 DEBUG 7919 --- [nio-8080-exec-5] o.h.e.i.AbstractFlushingEventListener : Dirty checking collections
2024-01-26T02:42:26.517+09:00 DEBUG 7919 --- [nio-8080-exec-5] o.h.e.i.AbstractFlushingEventListener : Flushed: 0 insertions, 0 updates, 0 deletions to 1 objects
2024-01-26T02:42:26.517+09:00 DEBUG 7919 --- [nio-8080-exec-5] o.h.e.i.AbstractFlushingEventListener : Flushed: 0 (re)creations, 0 updates, 0 removals to 0 collections
2024-01-26T15:59:16.034+09:00 DEBUG 5162 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Not closing pre-bound JPA EntityManager after transaction
MemberEntityService의 saveMember1 메서드에 적용된 트랜잭션이 실행된다. 그리고 save 내부의 트랜잭션은 기본 전파 옵션으로 인해 saveMember1 메서드에 적용된 트랜잭션에 참여한다.
트랜잭션 시작(MemberEntityService) → save 메서드의 트랜잭션은 MemberEntityService의 @Transactional 트랜잭션에 참여
→ 로그 → 커밋 → 더티체킹 → 플러시
// 2
2024-01-26T02:36:22.678+09:00 DEBUG 7919 --- [nio-8080-exec-3] o.h.e.t.internal.TransactionImpl : On TransactionImpl creation, JpaCompliance#isJpaTransactionComplianceEnabled == false
2024-01-26T02:36:22.678+09:00 DEBUG 7919 --- [nio-8080-exec-3] o.h.e.t.internal.TransactionImpl : begin
2024-01-26T02:36:22.678+09:00 DEBUG 7919 --- [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@76802679]
2024-01-26T02:36:22.678+09:00 DEBUG 7919 --- [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Found thread-bound EntityManager [SessionImpl(1192055710<open>)] for JPA transaction
2024-01-26T02:36:22.678+09:00 DEBUG 7919 --- [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Participating in existing transaction
2024-01-26T02:36:22.678+09:00 DEBUG 7919 --- [nio-8080-exec-3] o.h.j.internal.PersistenceUnitUtilImpl : jakarta.persistence.PersistenceUnitUtil.getIdentifier is only intended to work with enhanced entities (although Hibernate also adapts this support to its proxies); however the passed entity was not enhanced (nor a proxy).. may not be able to read identifier
2024-01-26T02:36:22.679+09:00 DEBUG 7919 --- [nio-8080-exec-3] org.hibernate.engine.spi.ActionQueue : Executing identity-insert immediately
2024-01-26T02:36:22.679+09:00 DEBUG 7919 --- [nio-8080-exec-3] org.hibernate.SQL : insert into member_entity (name,member_id) values (?,default)
2024-01-26T02:36:22.680+09:00 DEBUG 7919 --- [nio-8080-exec-3] o.h.id.IdentifierGeneratorHelper : Natively generated identity (com.example.demo.MemberEntity) : 2
2024-01-26T02:36:22.680+09:00 DEBUG 7919 --- [nio-8080-exec-3] o.h.r.j.i.ResourceRegistryStandardImpl : HHH000387: ResultSet's statement was not registered
2024-01-26T02:36:22.680+09:00 DEBUG 7919 --- [nio-8080-exec-3] o.h.r.j.i.ResourceRegistryStandardImpl : Exception clearing maxRows/queryTimeout [The object is already closed [90007-224]]
2 - Transactional
2024-01-26T02:36:22.680+09:00 DEBUG 7919 --- [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Initiating transaction commit
2024-01-26T02:36:22.680+09:00 DEBUG 7919 --- [nio-8080-exec-3] o.s.orm.jpa.JpaTransactionManager : Committing JPA transaction on EntityManager [SessionImpl(1192055710<open>)]
2024-01-26T02:36:22.680+09:00 DEBUG 7919 --- [nio-8080-exec-3] o.h.e.t.internal.TransactionImpl : committing
2024-01-26T15:59:16.034+09:00 DEBUG 5162 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Not closing pre-bound JPA EntityManager after transaction
쓰기연산에 @Transactional(readOnly = true)를 적용하고 save를 실행했다. 처음에는 readOnly = true인 경우에는 더티체킹, 플러시가 적용되지 않아 MemberEntity가 저장되지 않을 것이라 예상했다. 그러나 실제로 실행했을 때는 MemberEntity가 저장되었다.
왜 그런지 며칠동안 알아보다가 공식문서에서 이유를 찾을 수 있었다.
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.
반드시 쓰기 연산이 실패하는 것은 아니다. 읽기 전용 힌트를 해석할 수 없는 트랜잭션 매니저
는 읽기 전용 트랜잭션을 요청할 때 예외를 발생 시키지 않기 때문이다.
이 상황에서 왜 트랜잭션 매니저는 읽기 전용 힌트를 해석할 수 없을까?
정답은 데이터베이스가 H2
이기 때문이다.
org/springframework/jdbc/datasource/DataSourceTransactionManager.java:377
를 보면 다음과 같은 코드가 있다.
protected void prepareTransactionalConnection(Connection con, TransactionDefinition definition)
throws SQLException {
if (isEnforceReadOnly() && definition.isReadOnly()) {
Statement stmt = con.createStatement();
try {
stmt.executeUpdate("SET TRANSACTION READ ONLY");
}
finally {
stmt.close();
}
}
}
read-only 설정을 했다면 SET TRANSACTION READ ONLY 라는 질의를 실행하는데 H2 JDBC 드라이버에서는 이게 처리 안된다고 예상한다.
트랜잭션 시작(MemberEntityService) → save 메서드의 트랜잭션은 MemberEntityService의 @Transactional(readOnly = true) 트랜잭션에 참여
→ 로그 → 커밋
MySQL에서 실행해보면 예상대로 플러시가 적용되지 않아 에러가 발생한다.
디버깅을 해보니 트랜잭션이 끝나는 시점이 아니라 save() 메서드가 적용되는 시점
에서 예외가 발생한다.
java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
Hibernate는 readOnly 옵션이 설정된 경우는 Session의 Flush Mode를 'FlushMode.MANUAL' 모드로 설정한다.
H2, MySQL 둘 다 MemberEntity가 저장됐다.
// 3
2024-01-26T02:35:14.390+09:00 DEBUG 7919 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2024-01-26T02:35:14.391+09:00 DEBUG 7919 --- [nio-8080-exec-1] o.h.e.t.internal.TransactionImpl : On TransactionImpl creation, JpaCompliance#isJpaTransactionComplianceEnabled == false
2024-01-26T02:35:14.391+09:00 DEBUG 7919 --- [nio-8080-exec-1] o.h.e.t.internal.TransactionImpl : begin
2024-01-26T02:35:14.391+09:00 DEBUG 7919 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@2ec95929]
2024-01-26T02:35:14.393+09:00 DEBUG 7919 --- [nio-8080-exec-1] o.h.j.internal.PersistenceUnitUtilImpl : jakarta.persistence.PersistenceUnitUtil.getIdentifier is only intended to work with enhanced entities (although Hibernate also adapts this support to its proxies); however the passed entity was not enhanced (nor a proxy).. may not be able to read identifier
2024-01-26T02:35:14.397+09:00 DEBUG 7919 --- [nio-8080-exec-1] org.hibernate.engine.spi.ActionQueue : Executing identity-insert immediately
2024-01-26T02:35:14.399+09:00 DEBUG 7919 --- [nio-8080-exec-1] org.hibernate.SQL : insert into member_entity (name,member_id) values (?,default)
2024-01-26T02:35:14.407+09:00 DEBUG 7919 --- [nio-8080-exec-1] o.h.id.IdentifierGeneratorHelper : Natively generated identity (com.example.demo.MemberEntity) : 1
2024-01-26T02:35:14.414+09:00 DEBUG 7919 --- [nio-8080-exec-1] o.h.r.j.i.ResourceRegistryStandardImpl : HHH000387: ResultSet's statement was not registered
2024-01-26T02:35:14.414+09:00 DEBUG 7919 --- [nio-8080-exec-1] o.h.r.j.i.ResourceRegistryStandardImpl : Exception clearing maxRows/queryTimeout [The object is already closed [90007-224]]
2024-01-26T02:35:14.416+09:00 DEBUG 7919 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Initiating transaction commit
2024-01-26T02:35:14.416+09:00 DEBUG 7919 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Committing JPA transaction on EntityManager [SessionImpl(1216854118<open>)]
2024-01-26T02:35:14.416+09:00 DEBUG 7919 --- [nio-8080-exec-1] o.h.e.t.internal.TransactionImpl : committing
2024-01-26T02:35:14.416+09:00 DEBUG 7919 --- [nio-8080-exec-1] o.h.e.i.AbstractFlushingEventListener : Processing flush-time cascades
2024-01-26T02:35:14.417+09:00 DEBUG 7919 --- [nio-8080-exec-1] o.h.e.i.AbstractFlushingEventListener : Dirty checking collections
2024-01-26T02:35:14.418+09:00 DEBUG 7919 --- [nio-8080-exec-1] o.h.e.i.AbstractFlushingEventListener : Flushed: 0 insertions, 0 updates, 0 deletions to 1 objects
2024-01-26T02:35:14.418+09:00 DEBUG 7919 --- [nio-8080-exec-1] o.h.e.i.AbstractFlushingEventListener : Flushed: 0 (re)creations, 0 updates, 0 removals to 0 collections
2024-01-26T02:35:14.419+09:00 DEBUG 7919 --- [nio-8080-exec-1] o.hibernate.internal.util.EntityPrinter : Listing entities:
2024-01-26T02:35:14.419+09:00 DEBUG 7919 --- [nio-8080-exec-1] o.hibernate.internal.util.EntityPrinter : com.example.demo.MemberEntity{MemberId=1, name=32265}
2024-01-26T02:35:14.420+09:00 DEBUG 7919 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Not closing pre-bound JPA EntityManager after transaction
3 - Transactional 완료
H2에서는 SimpleJpaRepository의 save 메서드에서 트랜잭션 생성되고 커밋, 플러시가 실행되어 MemberEntity가 저장된다. 그 이후 로그가 찍힌다.
트랜잭션 시작(SimpleJpaRepository) → 커밋 → 더티체킹 → 플러시 → 로그
쓰기 연산에 @Transactional(readOnly = true)를 사용하면 데이터베이스 벤더에 따라 다르다. H2는 트랜잭션 매니저가 읽기 전용 힌트를 해석하지 못해 쓰기 연산이 실행되지만, MySQL은 트랜잭션 매니저가 읽기 전용 힌트를 해석할 수 있어 쓰기 연산이 실행되지 않는다.
@Transactional 없이 쓰기 연산을 하면 데이터베이스 벤더에 상관없이 save의 트랜잭션으로 인해 쓰기 연산이 실행된다.
https://github.com/scratchstudio/toby-spring/issues/7
https://tech.yangs.kr/22
https://deveric.tistory.com/86
https://www.blog.kcd.co.kr/jpa-영속성-컨텍스트와-osiv-3c5521e6de9f
https://docs.spring.io/spring-framework/docs/3.0.x/javadoc-api/org/springframework/transaction/annotation/Transactional.html#readOnly()