[Springboot] 서비스에 @Transactional vs 서비스에 @Transactional(readOnly = true) vs 서비스에 트랜잭션을 선언하지 않은 경우에 JPA save 동작 과정

일단 해볼게·2024년 1월 26일
0

Springboot

목록 보기
6/26

일반적으로 서비스 레벨에서 쓰기 연산(CUD)에 @Transactional 옵션을, 읽기 연산에 @Transactional(readOnly = true)을 사용한다. @Transactional의 readOnly 기본 옵션은 false이다. 만약 쓰기 연산에 @Transactional(readOnly = true)를 사용하면 어떻게 될까? 또한 @Transactional 없이 쓰기 연산을 하면 어떻게 되는지 알아보자.

Spring Data Jpa에서 제공하는 JpaRepository의 기본 구현체는 SimpleJpaRepository이다. CRD 메서드마다 @Transactional 옵션이 다르니 알아보자.

create(save)

save 메서드에 @Transactional이 선언되어있다. 따라서 readOnly = false이다. saveAndFlush, saveAll, saveAllAndFlush도 마찬가지다.

delete

delete 메서드에도 @Transactional이 선언되어있다. 따라서 readOnly = false이다. deleteAllById, deleteAllByIdInBatch, deleteAll, deleteAllInBatch도 마찬가지다.

read(find)

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. 쓰기연산에 @Transactional을 적용하는 경우

// 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. 쓰기연산에 @Transactional(readOnly = true)를 적용하는 경우

H2 database

// 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) 트랜잭션에 참여 → 로그 → 커밋

  • 더티체킹, 플러시 X

MySQL

MySQL에서 실행해보면 예상대로 플러시가 적용되지 않아 에러가 발생한다.
디버깅을 해보니 트랜잭션이 끝나는 시점이 아니라 save() 메서드가 적용되는 시점에서 예외가 발생한다.

java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed

Hibernate는 readOnly 옵션이 설정된  경우는 Session의 Flush Mode를 'FlushMode.MANUAL' 모드로 설정한다.

3. 쓰기 연산에 @Transactional을 적용하지 않는 경우

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()

적용한 프로젝트

https://github.com/bjo6300/transactional-example

profile
시도하고 More Do하는 백엔드 개발자입니다.

0개의 댓글