JAVA/JAVA | Spring 학습기록

[Spring] @Transactional(readOnly=true)에서 write 시 예외 좀 더 살펴보기

kth990303 2024. 1. 22. 12:01
반응형

개발하던 도중 아래 에러를 만났다.

 

The MySQL server is running with the --read-only option so it cannot execute this statement

 

에러 발생 원인은 @Transactional(readOnly=true) 어노테이션이 붙어진 메서드에서 save 로직을 했기 때문이다.

 

근데 이 readOnly 옵션에 대해 의견이 분분했다.

그래서 이번 기회에 조금 더 살펴보려 한다.

 

코드 쪽을 쭉 살펴보면서 분석해봤으며, 혼자서 분석해본 것이라 틀린 내용이 포함될 수 있다.

 

 

결론부터 말하자면 아래와 같다.

  • H2 DB는 readOnly 무시됨. (정말 유명한 사실이다.)
  • MySQL, MariaDB는 readOnly 시 락 걸고, 여기에 위배되는 로직 작성 시 DB에서 SQLException 내뱉음.
    • 이 때, 버전에 대한 차이 없음! (일부 다른 블로그글에서 5.6.5 버전부터 예외 발생한다고 되어있는데, 모르겠다... 일단 5.1 버전에도 SQLException 내뱉는 로직은 있긴 했음.)
    • 다만, SQLException은 RuntimeException 이 아닌 checked exception이므로 스프링 @Transactional 내에서 롤백이 안되니까 별도로 예외처리 잘 해줄 것.

@Transactional readOnly 내부 코드

 

공식 문서에는 아래와 같이 되어있다.

 * A transaction manager which cannot interpret the read-only hint will
 * not throw an exception when asked for a read-only transaction
 * but rather silently ignore the hint.

 

즉, read-only를 DB 벤더 JDBC 드라이버 구현체마다 꼭 필수적으로 지켜야되는 것은 아니라는 것.

심지어 read-only 자체를 무시해도 상관은 없어보인다.

대표적인 예가 바로 h2 DB.

ignored 라고 대놓고 명시하고 있음.

H2 DB의 setReadOnly 코드를 보자.

(사실 코드 볼 필요도 없는게, Params: readOnly - ignored 한다고 되어있음 ㅋㅋㅋ)

 

해당 코드의 역할은 두 가지.

  • debugCode
  • checkClosed

 

근데 debugCode는 코드를 타고 들어가보면, 디버깅 시 로그 남기는 데에만 사용되는 코드이고

checkClosed에는 readOnly 파라미터를 넘겨주지도 않는다.

 

따라서 h2에서는 readOnly가 읽기전용 transaction 세션으로 만드는 역할은 전혀 하지 않고 있다.


MySQL, MariaDB에선 어떻게 되고 있을까?

 

DataSourceUtils#setReadOnly 에 대해 각각의 벤더 구현체 오버라이딩 코드를 타고 들어가보자. 

mariaDB는 Connection, mysql은 ConnectionImpl로 가면 되겠다.

 

MySQL

클릭해서 확대해서 보는 걸 권장

 

 

MariaDB

 

 

둘 다 readOnly일 경우 읽기 전용으로 락을 걸어주는 것을 확인할 수 있다.

 

그리고 조금 더 파고 들어가면, 예외 문구도 잘 띄워준다.

 

1. gradle version (구버전)

implementation 'mysql:mysql-connector-java:5.1.6'

 

PreparedStatement # execute

 

참고로 PreparedStatement.20 예외 메시지는 아래 사진에서 볼 수 있듯이, Connection is read-only이다.

 

5.1 버전에서도 예외는 잘 던져주고 있긴 한데?

 

혹시나 싶어서 mysql 5.6.5 release notes를 읽어봤는데 딱히 5.6.5부터 readOnly가 적용된다는 글은 못찾았다.

그리고 아래 글도 발견했다.

5.6버전부터 read only 트랜잭션에서 오버헤드가 덜 든다!

 

이 말인 즉슨, 5.6 이전버전에도 ReadOnly 트랜잭션은 있었긴 하다는건데.

 

일단 버전 차이에 따른 SQLException 예외 여부는 잘 못찾았다.

걍 버전 차이 없이 mysql, mariadb에선 readOnly에서 CUD 명령이 들어오면 예외를 던지는 것으로 기억할련다...

(이 부분 혹시 알고 계신 분 있으면 편하게 댓글 부탁드립니다!)

 

+) 240126 추가

주형님이 조금 더 상세하게 원인 파악 및 문서를 읽어봐주셨다.

https://dkswnkk.tistory.com/740

 

MySQL 버전에 따른 @Transactional(readOnly=true)의 동작 과정

개요 이 글은 태현님의 블로그 게시글에서 영감을 받아 작성하게 되었습니다. 우리는 스프링 프레임워크를 통해 RDBMS를 활용하는 과정에서, 우리는 성능 최적화, 가독성 향상, 데이터 일관성 유

dkswnkk.tistory.com

 

5.6.5 버전 기준 이후부터 MySQL 서버에 읽기전용 성질을 전파할 것인지 여부의 코드가 존재한다.

(다만, 5.6.5 버전 이하에도 JDBC 드라이버에서 읽기전용으로 처리되기 때문에 SQLException은 터진다. MySQL 서버에서 터지냐, JDBC Driver 단에서 처리하냐의 차이일 뿐.)


2. gradle version (최신)

runtimeOnly 'com.mysql:mysql-connector-j'

 

 

 

최신 버전인 만큼, 마찬가지로 SQLException 잘 만들고 예외 메시지도 잘 만들어준다.


 

참고로 위에서도 언급하기도 했고, 이번 주제에선 벗어나는 얘기지만

SQLException은 checked Exception이므로 여러 query가 존재한다면 원하는대로 롤백이 안될 수 있으니 주의!

 

반응형