관리 메뉴

Mini

H2 db에서 @transactional(readOnly)이 적용되지않는 문제 본문

카테고리 없음

H2 db에서 @transactional(readOnly)이 적용되지않는 문제

Mini_96 2025. 4. 24. 15:22

문제상황

jpashop의 배포환경에서 상품목록 페이지에 들어간후 수정버튼을 클릭하니 500error 가 발생하였다.

배포서버의 로그(output.log)를 확인해보니 아래와 같았다.

Hibernate: 
    select
        item0_.item_id as item_id2_3_0_,
        item0_.name as name3_3_0_,
        item0_.price as price4_3_0_,
        item0_.stock_quantity as stock_qu5_3_0_,
        item0_.artist as artist6_3_0_,
        item0_.etc as etc7_3_0_,
        item0_.author as author8_3_0_,
        item0_.isbn as isbn9_3_0_,
        item0_.actor as actor10_3_0_,
        item0_.director as directo11_3_0_,
        item0_.dtype as dtype1_3_0_ 
    from
        item item0_ 
    where
        item0_.item_id=? for update
            
2025-04-16 17:51:16.524  INFO 121675 --- [nio-8080-exec-9] p6spy                                    : #1744825876524 | took 11ms | statement | connection 19| url jdbc:mysql://localhost:3306/jpashop
select item0_.item_id as item_id2_3_0_, item0_.name as name3_3_0_, item0_.price as price4_3_0_, item0_.stock_quantity as stock_qu5_3_0_, item0_.artist as artist6_3_0_, item0_.etc as etc7_3_0_, item0_.author as author8_3_0_, item0_.isbn as isbn9_3_0_, item0_.actor as actor10_3_0_, item0_.director as directo11_3_0_, item0_.dtype as dtype1_3_0_ from item item0_ where item0_.item_id=? for update
select item0_.item_id as item_id2_3_0_, item0_.name as name3_3_0_, item0_.price as price4_3_0_, item0_.stock_quantity as stock_qu5_3_0_, item0_.artist as artist6_3_0_, item0_.etc as etc7_3_0_, item0_.author as author8_3_0_, item0_.isbn as isbn9_3_0_, item0_.actor as actor10_3_0_, item0_.director as directo11_3_0_, item0_.dtype as dtype1_3_0_ from item item0_ where item0_.item_id=3 for update;
2025-04-16 17:51:16.525  WARN 121675 --- [nio-8080-exec-9] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 1792, SQLState: 25006
2025-04-16 17:51:16.525 ERROR 121675 --- [nio-8080-exec-9] o.h.engine.jdbc.spi.SqlExceptionHelper   : Cannot execute statement in a READ ONLY transaction.
2025-04-16 17:51:16.533  INFO 121675 --- [nio-8080-exec-9] o.h.e.internal.DefaultLoadEventListener  : HHH000327: Error performing load command

org.hibernate.exception.GenericJDBCException: could not extract ResultSet

 

 

문제 원인

findOne의 Transactional이 클래스단위의 readOnly=true로 적용되어있었다.

낙관적락 속성으로 발생된 select for update 쿼리가 읽기전용 트랜잭션에서 실행되어 에러가 발생한것으로 보인다.

배포환경의 db는 mysql이다.

@Repository
@RequiredArgsConstructor
@Transactional(readOnly = true) //읽기전용 트랜잭션
public class ItemRepository {
	
    public Item findOne(Long id){
        return em.find(Item.class, id, LockModeType.PESSIMISTIC_WRITE);
    }

 

로컬에서는 잘 작동했던 이유

로컬에서는 상품수정 폼이 정상적으로 작동하였다. 이 때문에 배포되고 나서야 에러를 발견할 수 있었다.

원인은 H2 database에 있었다. 로컬환경에서는 H2 db 에 연결되있는데, H2의 Connection 구현체에서 이는 무시된다고 적혀있다.

즉, 로컬의 H2에서는 findOne 메서드에 Transactional(readOnly=false)를 걸지 않아도 잘 작동하였으나,

배포환경의 mysql에서는 Transactional(readOnly=true)에서 쓰기락을 얻으려해서, SQL에러가 발생한것이다.

해결

해결방법은 findOne 메서드에 @Transactional(readOnly=false)를 걸어주면 된다.

 

 

테스트코드에서 걸러지지 못한이유

애초에 테스트코드에서 발견됬어야할 문제가 아닌가? 살펴보자.

@ActiveProfiles("test")
@SpringBootTest
@Transactional
class ItemRepositoryTest {

    @Test
    void findOne() {
        // given
        Book book = new Book();
        book.setName("JPA");
        book.setPrice(10000);
        book.setStockQuantity(10);
        itemRepository.save(book);

        // when
        Item findItem = itemRepository.findOne(book.getId());

        // then
        assertThat(findItem).extracting(
                "id",
                "name",
                "price",
                "stockQuantity"
        ).containsExactly(
                book.getId(),
                book.getName(),
                book.getPrice(),
                book.getStockQuantity()
        );
    }

예상과달리, TestCode도 통과한것을 볼 수 있었다.

test환경을 mysql로 바꾸면 에러가 발생되는지 보겠다. 이를위해 @ActiveProfile을 (local)로 바꿔보았다.

결과는 mysql로 변경해도 테스트가 통과해버리는 문제가 여전히 남아있다.

추정원인

원인은 TestCode에 있는 @Transactional이 원인이었다. 기본값이 readOnly false로 되어있어, 해당 트랜잭션이 전파되어 테스트가 통과된 것이다.

Test Code에서 @Transactional을 주석처리하면, 에러를 얻을 수 있었다.

 

정리

TestCode에 @Transactional은 자동롤백등 많은 이점을 주지만, 실제서비스와는 다르게 트랜잭션이 동작될 수 있기에 잘 알고 사용해야한다. (예시 : service에는 transactional이 읽기전용인데, test는 읽기전용이 아닌것으로 실행됨)

개발환경에서도 H2 DB보다는 배포환경과 동일한 DB 사용이 권장된다. (이 내용은 나중에 다시 쓸여질수도 있다..)

h2 환경에서 테스트가 통과하더라도 mysql 배포환경에서도 잘 작동한다고 보장 할 수 없다.

 

참고

@Transactional(readOnly=true)는 변경감지용 스냅샷을 보관하지 않고, 커밋시 flush가 호출되지 않는다.

 

 

레퍼런스

https://github.com/h2database/h2database/blob/master/h2/src/main/org/h2/jdbc/JdbcConnection.java#L559

 

h2database/h2/src/main/org/h2/jdbc/JdbcConnection.java at master · h2database/h2database

H2 is an embeddable RDBMS written in Java. Contribute to h2database/h2database development by creating an account on GitHub.

github.com

https://mangkyu.tistory.com/367

 

[Java] H2 데이터베이스에서 @Transactional(readOnly=true)일때 save를 호출하는 경우

1. H2 데이터베이스에서 @Transactional(readOnly=true)일때 save를 호출하는 경우[ H2 데이터베이스에서 @Transactional(readOnly=true)일때 save를 호출하는 경우 ]다음과 같이 트랜잭션을 시작하고 데이터를 저장

mangkyu.tistory.com

https://ssisksl77.gitlab.io/code-that-talks/220526-spring-transactional-readonly.html

 

h2에서 @Transactional(readOnly=true) 가 안먹히는 이유 - Why @Transactional(readOnly=true) is not working in H2? (proble

당연히 트랜잭션이 수행될 때, isReadOnly() 를 사용할 것이다. package org.springframework.orm.jpa.vendor; public class HibernateJpaDialect extends DefaultJpaDialect { // ... @Override public Object beginTransaction(EntityManager entityMang

ssisksl77.gitlab.io

https://docs.spring.io/spring-framework/docs/4.3.x/javadoc-api/org/springframework/transaction/annotation/Transactional.html#readOnly--

 

Transactional (Spring Framework 4.3.30.RELEASE API)

A boolean flag that can be set to true if the transaction is effectively read-only, allowing for corresponding optimizations at runtime. Defaults to false. This just serves as a hint for the actual transaction subsystem; it will not necessarily cause failu

docs.spring.io