티스토리 뷰

Java & Kotlin

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

망나니개발자 2024. 6. 11. 10:00
반응형

 

 

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


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

다음과 같이 트랜잭션을 시작하고 데이터를 저장하는 로직이 있다고 하자. 그런데 실수로 @Transactional을 readOnly로 설정하고 작업했다면 어떻게 동작할까?

@RestController
class SystemController(
    private val memberRepository: MemberRepository,
) {

    @GetMapping("/member")
    @Transactional(readOnly = true)
    fun health() {
        memberRepository.save(Member("MangKyu"))
    }
}

 


MySQL 데이터베이스를 사용하는 경우에, @Transactional(readOnly=true)인 상황에서 데이터를 저장하려고 하면 다음과 같은 에러가 발생한다. 해당 에러가 발생하는 이유는 데이터베이스 연결을 읽기 전용으로 설정하기 때문에 데이터를 저장할 수 없기 때문이다.

Caused by: java.sql.SQLException: The MySQL server is running with the --read-only option so it cannot execute this statement
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:129) ~[mysql-connector-java-8.0.29.jar:8.0.29]
	at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122) ~[mysql-connector-java-8.0.29.jar:8.0.29]
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:916) ~[mysql-connector-java-8.0.29.jar:8.0.29]
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdateInternal(ClientPreparedStatement.java:1061) ~[mysql-connector-java-8.0.29.jar:8.0.29]
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdateInternal(ClientPreparedStatement.java:1009) ~[mysql-connector-java-8.0.29.jar:8.0.29]
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeLargeUpdate(ClientPreparedStatement.java:1320) ~[mysql-connector-java-8.0.29.jar:8.0.29]
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdate(ClientPreparedStatement.java:994) ~[mysql-connector-java-8.0.29.jar:8.0.29]
	at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeUpdate(ProxyPreparedStatement.java:61) ~[HikariCP-4.0.3.jar:na]
	at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeUpdate(HikariProxyPreparedStatement.java) ~[HikariCP-4.0.3.jar:na]

 

 

하지만 데이터베이스를 H2로 설정하고 진행하면 에러가 발생하지는 않는다. 대신 데이터가 정상적으로 저장되지는 않는데, 그 이유에 대해 살펴보고자 한다.

 

스프링의 Spring-Data 공식 문서를 보면 readOnly 옵션에 대해 다음과 같은 설명이 적혀 있다. readOnly=true 플래그는 (일부 데이터베이스의 경우 INSERT나 UPDATE 쿼리를 REJECT 시키긴하지만) 데이터를 변경하는 쿼리를 실행했음에 대한 검사를 의미하지는 않는다고 한다. 대신 성능 최적화를 위해 JDBC 드라이버에게 힌트를 전파하기 위해 전달된다고 한다.

 

 

따라서 연결하고자 하는 데이터베이스 그리고 JDBC 드라이버에 따라 동작이 달라질 수 있고, MySQL의 경우 쿼리를 REJECT 시키는 것이다. 반면에 H2의 경우 이를 막지 않고 실제로 데이터가 저장되기까지 한다.

H2의 Connection 구현체에서 readOnly를 설정하는 코드의 설명을 보면, 해당 설정에 대한 내용을 무시한다는 내용을 확인할 수 있다.

/**
* According to the JDBC specs, this setting is only a hint to the database
* to enable optimizations - it does not cause writes to be prohibited.
*
* @param readOnly ignored
* @throws SQLException if the connection is closed
*/
@Override
public void setReadOnly(boolean readOnly) throws SQLException {
    try {
        if (isDebugEnabled()) {
            debugCode("setReadOnly(" + readOnly + ')');
        }
        checkClosed();
    } catch (Exception e) {
        throw logAndConvert(e);
    }
}

 

 

그리고 H2의 동작 방식을 확인해보면, readOnly=true로 설정된 경우에는 데이터베이스 내부적으로 변경점이 없다고 판단한다. 따라서 commit을 수행하지 않고 무시한다. 즉, 에러를 발생하는 MySQL과는 달리 H2는 커밋을 수행하지 않기 때문에 에러가 발생하지는 않고 데이터가 저장까지 되지는 않는 것이다. 관련된 코드는 H2 데이터베이스의 트랜잭션 클래스에서 확인할 수 있다.

public void commit() {
    assert store.openTransactions.get().get(transactionId);
    markTransactionEnd();
    Throwable ex = null;
    boolean hasChanges = false;
    int previousStatus = STATUS_OPEN;
    try {
        long state = setStatus(STATUS_COMMITTED);
        hasChanges = hasChanges(state);
        previousStatus = getStatus(state);
        if (hasChanges) {
            store.commit(this, previousStatus == STATUS_COMMITTED);
        }
    catch (Throwable e) {
    
    ...

 

 

 

 

H2 데이터베이스 내부는 개인적인 궁금증으로 찾아본 것이고, 중요한 것은 readOnly 옵션이 트랜잭션의 변경 여부에 대한 검사 기능이 아닌, 성능 최적화를 위한 힌트로 제공된다는 것이다. 이는 JDBC 스펙이므로 구현체에 따라 달라질 수 있음을 명시하도록 하자.

 

 

 

 

 

반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG more
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함