Mini
H2 db에서 @transactional(readOnly)이 적용되지않는 문제 본문
문제상황
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가 호출되지 않는다.
레퍼런스
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
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