λ³Έλ¬Έ λ°”λ‘œκ°€κΈ°
BackEnd🌱/Spring

MySQL 버전에 λ”°λ₯Έ @Transactional(readOnly=true)의 λ™μž‘ κ³Όμ •

by μ•ˆμ£Όν˜• 2024. 1. 24.

κ°œμš”

이 글은 νƒœν˜„λ‹˜μ˜ λΈ”λ‘œκ·Έ κ²Œμ‹œκΈ€μ—μ„œ μ˜κ°μ„ λ°›μ•„ μž‘μ„±ν•˜κ²Œ λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

μš°λ¦¬λŠ” μŠ€ν”„λ§ ν”„λ ˆμž„μ›Œν¬λ₯Ό 톡해 RDBMSλ₯Ό ν™œμš©ν•˜λŠ” κ³Όμ •μ—μ„œ, μš°λ¦¬λŠ” μ„±λŠ₯ μ΅œμ ν™”, 가독성 ν–₯상, 데이터 일관성 μœ μ§€ λ“± μ—¬λŸ¬ 이유둜 λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ— @Transactional(readOnly=true) μ–΄λ…Έν…Œμ΄μ…˜μ„ 자주 μ‚¬μš©ν•˜κ³€ ν•©λ‹ˆλ‹€. 그런데 이 μ–΄λ…Έν…Œμ΄μ…˜μ€ μ–΄λ– ν•œ μ›λ¦¬λ‘œ λ™μž‘ν•˜λŠ” κ²ƒμΌκΉŒμš”? μ΄λ²ˆ κΈ€μ—μ„œλŠ” @Transactional(readOnly=true)의 JDBC λ‹¨κ³„μ—μ„œμ˜ λ™μž‘ 과정을 μœ„μ£Όλ‘œ μ‚΄νŽ΄λ³΄λ € ν•©λ‹ˆλ‹€.

 

 

λ™μž‘ κ³Όμ •

λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ— @Transactional(readOnly=true)을 κ±Έκ³  μ‹€ν–‰ν•˜λŠ” 경우 λ™μž‘ν•˜λŠ” 전체적인 과정은 μ•„λž˜μ™€ κ°™μŠ΅λ‹ˆλ‹€.

  1. νŠΈλžœμž­μ…˜ μ‹œμž‘
  2. λ°μ΄ν„°λ² μ΄μŠ€ μ—°κ²° μ€€λΉ„
  3. 읽기 μ „μš© μƒνƒœ μ „νŒŒ
  4. λΉ„μ¦ˆλ‹ˆμŠ€ 둜직 μ‹€ν–‰
  5. νŠΈλžœμž­μ…˜ μ’…λ£Œ

각 과정에 λŒ€ν•΄ μƒμ„Έν•˜κ²Œ ν•œλ²ˆ μ•Œμ•„λ³΄κ² μŠ΅λ‹ˆλ‹€.
 

1. νŠΈλžœμž­μ…˜ μ‹œμž‘

μ•„λž˜μ™€ 같이 @Transactional(readOnly=true)κ°€ μ„€μ •λœ λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ„ ν˜ΈμΆœν•˜λ©΄ νŠΈλžœμž­μ…˜μ΄ μ‹œμž‘λ©λ‹ˆλ‹€.

@Service
public class MyService {

    @Transactional(readOnly=true)
    public MyData getData(int id) {
        // λΉ„μ¦ˆλ‹ˆμŠ€ 둜직
    }
    
}

μœ„ λ©”μ„œλ“œκ°€ 호좜되면 μŠ€ν”„λ§μ˜ AOP ν”„λ ˆμž„μ›Œν¬κ°€ 이λ₯Ό μΈμ§€ν•˜κ³ , νŠΈλžœμž­μ…˜ 인터셉터λ₯Ό 톡해 νŠΈλžœμž­μ…˜μ„ μ‹œμž‘ν•©λ‹ˆλ‹€.

μŠ€ν”„λ§μ˜ νŠΈλžœμž­μ…˜ κ΄€λ¦¬λŠ” AOP(Aspect-Oriented Programming) 기반으둜 μž‘λ™ν•˜λ©°, 이λ₯Ό 톡해 λΉ„μ¦ˆλ‹ˆμŠ€ 둜직과 νŠΈλžœμž­μ…˜ 처리λ₯Ό λΆ„λ¦¬ν•˜μ—¬ μ½”λ“œμ˜ 가독성과 μž¬μ‚¬μš©μ„±μ„ ν–₯μƒμ‹œν‚€λŠ” 효과λ₯Ό 얻을 수 μžˆμŠ΅λ‹ˆλ‹€. 이 κ³Όμ •μ—μ„œ νŠΈλžœμž­μ…˜ μΈν„°μ…‰ν„°λŠ” 쀑좔적인 역할을 λ‹΄λ‹Ήν•˜λ©°, νŠΈλžœμž­μ…˜μ˜ μ‹œμž‘, μ’…λ£Œ, ν•„μš”μ‹œ λ‘€λ°± λ“±μ˜ μž‘μ—…μ„ μˆ˜ν–‰ν•©λ‹ˆλ‹€.

@Transactional(readOnly=true) μ–΄λ…Έν…Œμ΄μ…˜μ„ 톡해 getData λ©”μ„œλ“œκ°€ 호좜될 λ•Œ, νŠΈλžœμž­μ…˜ 인터셉터가 ν™œμ„±ν™”λ˜μ–΄ μƒˆλ‘œμš΄ νŠΈλžœμž­μ…˜μ„ μ‹œμž‘ν•©λ‹ˆλ‹€. μ΄λ•Œ νŠΈλžœμž­μ…˜μ˜ 속성듀(읽기 μ „μš©, 격리 μˆ˜μ€€, νƒ€μž„μ•„μ›ƒ, μ „νŒŒ 방식 λ“±)이 νŠΈλžœμž­μ…˜ λ§€λ‹ˆμ €μ—κ²Œ μ „λ‹¬λ©λ‹ˆλ‹€.
 

2. λ°μ΄ν„°λ² μ΄μŠ€ μ—°κ²° μ€€λΉ„

DataSourceTransactionManager의 doBegin()

νŠΈλžœμž­μ…˜μ΄ μ‹œμž‘λ˜λ©΄ μŠ€ν”„λ§μ˜ νŠΈλžœμž­μ…˜ κ΄€λ¦¬μžλŠ” DataSourceTransactionalManager.doBegin λ©”μ„œλ“œ λ‚΄λΆ€μ—μ„œ DataSourceUtils.prepareConnectionForTransaction λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•˜μ—¬ λ°μ΄ν„°λ² μ΄μŠ€ 연결을 μ€€λΉ„ν•©λ‹ˆλ‹€. 이 λ©”μ„œλ“œλŠ” μŠ€ν”„λ§ ν”„λ ˆμž„μ›Œν¬μ—μ„œ μ œκ³΅ν•˜λŠ” λ©”μ„œλ“œλ‘œ νŠΈλžœμž­μ…˜ μ •μ˜λ₯Ό λ°›μ•„ λ°μ΄ν„°λ² μ΄μŠ€ 연결을 ν™•λ³΄ν•˜κ³ , ν•΄λ‹Ή 연결에 νŠΈλžœμž­μ…˜μ˜ 속성듀을 μ μš©ν•˜λŠ” 역할을 ν•©λ‹ˆλ‹€.

// DataSourceUtils.java
public static Integer prepareConnectionForTransaction(Connection con, TransactionDefinition definition)
        throws SQLException {

    Assert.notNull(con, "No Connection specified");

    if (definition.isReadOnly()) {
        try {
            if (logger.isDebugEnabled()) {
                logger.debug("Setting JDBC Connection [" + con + "] read-only");
            }
            con.setReadOnly(true);
        }
        catch (SQLException | RuntimeException ex) {
            // μ˜ˆμ™Έ 처리
        }
    }
    // 격리 μˆ˜μ€€ μ„€μ • λ“±μ˜ μΆ”κ°€ μ½”λ“œ
}

DataSourceUtils의 prepareConnectionForTransaction

DataSourceUtils의 prepareConnectionForTransaction λ©”μ„œλ“œ λ‚΄μ—μ„œ definition.isReadOnly()λŠ” @Transactional(readOnly=true) μ–΄λ…Έν…Œμ΄μ…˜μ˜ 값이 true인지 ν™•μΈν•˜λŠ” λΆ€λΆ„μž…λ‹ˆλ‹€. 이 값이 true일 경우, con.setReadOnly(true)κ°€ ν˜ΈμΆœλ˜μ–΄ ν•΄λ‹Ή λ°μ΄ν„°λ² μ΄μŠ€ 연결이 읽기 μ „μš© λͺ¨λ“œλ‘œ μ„€μ •λ©λ‹ˆλ‹€. 이 섀정에 따라 ν•΄λ‹Ή 연결을 톡해 μˆ˜ν–‰λ˜λŠ” λͺ¨λ“  SQL λͺ…령은 데이터 λ³€κ²½ 없이 읽기 μ „μš© λͺ¨λ“œλ‘œ λ™μž‘ν•˜κ²Œ λ©λ‹ˆλ‹€.

λ˜ν•œ 이 λ©”μ„œλ“œλŠ” νŠΈλžœμž­μ…˜ μ •μ˜μ— 따라 λ°μ΄ν„°λ² μ΄μŠ€ μ—°κ²°μ˜ 격리 μˆ˜μ€€μ„ μ„€μ •ν•˜λŠ” 뢀뢄도 ν¬ν•¨ν•˜κ³  μžˆμ–΄, νŠΈλžœμž­μ…˜μ˜ 격리 μˆ˜μ€€μ΄ μ •μ˜λ˜μ–΄ μžˆλ‹€λ©΄, ν•΄λ‹Ή 격리 μˆ˜μ€€μ΄ λ°μ΄ν„°λ² μ΄μŠ€ 연결에 μ μš©λ©λ‹ˆλ‹€.

μ΄λ ‡κ²Œ λͺ¨λ“  과정이 μ„±κ³΅μ μœΌλ‘œ μ™„λ£Œλ˜λ©΄, νŠΈλžœμž­μ…˜μ„ μœ„ν•œ μ€€λΉ„κ°€ λͺ¨λ‘ 마무리된 μƒνƒœμž…λ‹ˆλ‹€. μ΄ν›„μ˜ μ²˜λ¦¬λŠ” 이 λ°μ΄ν„°λ² μ΄μŠ€ 연결을 톡해 이루어지며, νŠΈλžœμž­μ…˜μ΄ μ’…λ£Œλ  λ•ŒκΉŒμ§€ 이 연결은 μœ μ§€λ©λ‹ˆλ‹€.  νŠΈλžœμž­μ…˜μ΄ μ’…λ£Œλœ ν›„μ—λŠ” resetConnectAfterTransaction λ©”μ„œλ“œλ₯Ό 톡해 μ—°κ²° μƒνƒœκ°€ μ›λž˜λŒ€λ‘œ λ³΅μ›λ©λ‹ˆλ‹€.
 

3. 읽기 μ „μš© μƒνƒœ μ „νŒŒ

읽기 μ „μš©μœΌλ‘œ μ„€μ •λœ λ°μ΄ν„°λ² μ΄μŠ€ 연결은 MySQL의 JDBC λ“œλΌμ΄λ²„μ— μ „λ‹¬λ˜λ©° JDBC λ“œλΌμ΄λ²„λŠ” Connection.setReadOnly(true) ν˜ΈμΆœμ„ μΈμ§€ν•˜κ³ , λ‚΄λΆ€μ μœΌλ‘œ ConnectionImpl.setReadOnlyInternal λ©”μ„œλ“œλ₯Ό μ‹€ν–‰ν•©λ‹ˆλ‹€.

ConnectionImpl.setReadOnlyInternal λ©”μ„œλ“œλŠ” MySQL의 JDBC λ“œλΌμ΄λ²„μ—μ„œ μ œκ³΅ν•˜λŠ” λ©”μ„œλ“œλ‘œ, λ°μ΄ν„°λ² μ΄μŠ€ μ—°κ²°μ˜ 읽기 μ „μš© μƒνƒœλ₯Ό μ„€μ •ν•˜λŠ” 데 μ‚¬μš©λ©λ‹ˆλ‹€. 이 λ©”μ„œλ“œλŠ” MySQL μ„œλ²„μ˜ 버전을 ν™•μΈν•œ ν›„, 읽기 μ „μš© μƒνƒœλ₯Ό MySQL μ„œλ²„μ— μ „νŒŒν•˜λ €κ³  μ‹œλ„ν•©λ‹ˆλ‹€.

// ConnectionImpl.java
@Override
public void setReadOnlyInternal(boolean readOnlyFlag) throws SQLException {
    synchronized (getConnectionMutex()) {
        // note this this is safe even inside a transaction
        if (this.readOnlyPropagatesToServer.getValue() && versionMeetsMinimum(5, 6, 5)) {
            if (!this.useLocalSessionState.getValue() || readOnlyFlag != this.readOnly) {
                this.session.execSQL(null, "set session transaction " + (readOnlyFlag ? "read only" : "read write"), -1, null, false,
                        this.nullStatementResultSetFactory, null, false);
            }
        }
        this.readOnly = readOnlyFlag;
    }
}

ConnectionImpl의 setReadOnlyInternal

μœ„ μ½”λ“œμ—μ„œ versionMeetsMinimum(5, 6, 5)λŠ” MySQL μ„œλ²„μ˜ 버전이 5.6.5 이상인지 ν™•μΈν•˜λŠ” λΆ€λΆ„μž…λ‹ˆλ‹€. λ§Œμ•½ 5.6.5 버전 이상일 경우 execSQL(..., "set session transaction read only", ...)κ°€ ν˜ΈμΆœλ˜μ–΄, 읽기 μ „μš© μƒνƒœλ₯Ό MySQL μ„œλ²„μ— μ „λ‹¬ν•˜λŠ” SQL λͺ…령이 μ‹€ν–‰λ©λ‹ˆλ‹€.

이 과정을 톡해 JDBC λ“œλΌμ΄λ²„λŠ” λ°μ΄ν„°λ² μ΄μŠ€ μ—°κ²°μ˜ 읽기 μ „μš© μƒνƒœλ₯Ό MySQL μ„œλ²„μ— μ „νŒŒν•©λ‹ˆλ‹€. 이 과정은 JDBC λ“œλΌμ΄λ²„μ™€ MySQL μ„œλ²„ κ°„μ˜ 톡신 κ³Όμ •μ—μ„œ 이루어지며, 이λ₯Ό 톡해 MySQL μ„œλ²„λ„ ν•΄λ‹Ή 연결이 읽기 μ „μš©μž„μ„ μΈμ§€ν•˜κ²Œ λ©λ‹ˆλ‹€.

μ—¬κΈ°μ„œ λ§Œμ•½ MySQL μ„œλ²„μ˜ 버전이 5.6.5 미만이라면, 'READ ONLY'와 'READ WRITE' νŠΈλžœμž­μ…˜ νŠΉμ„±μ΄ μ§€μ›λ˜μ§€ μ•Šμ•„ 쑰건문을 ν†΅κ³Όν•˜μ§€ λͺ»ν•΄ 읽기 μ „μš© μƒνƒœκ°€ MySQL μ„œλ²„μ— μ „νŒŒλ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. 

 

μ„œλ²„ 버전이 5.6.5 미만인 경우, 'READ ONLY'와 'READ WRITE' νŠΈλžœμž­μ…˜ νŠΉμ„±μ΄ μ§€μ›λ˜μ§€ μ•ŠλŠ”λ‹€.

MySQL 5.6.5 이전 λ²„μ „μ—μ„œλŠ” μ„Έμ…˜ λ ˆλ²¨μ—μ„œλ§Œ 읽기 μ „μš© μƒνƒœλ₯Ό μ„€μ •ν•˜λŠ” 것이 κ°€λŠ₯ν–ˆμŠ΅λ‹ˆλ‹€. ν•˜μ§€λ§Œ, νŠΈλžœμž­μ…˜ μˆ˜μ€€μ—μ„œ 'READ ONLY'와 'READ WRITE' νŠΉμ„±μ„ μ§€μ •ν•˜λŠ” κΈ°λŠ₯은 MySQL 5.6.5 버전뢀터 μ œκ³΅λ˜μ—ˆμŠ΅λ‹ˆλ‹€.(https://forums.mysql.com/read.php?3,524924)

μ„Έμ…˜ λ ˆλ²¨μ—μ„œμ˜ 읽기 μ „μš© μƒνƒœμ™€ νŠΈλžœμž­μ…˜ μˆ˜μ€€μ—μ„œμ˜ 읽기 μ „μš© μƒνƒœλŠ” λ‹€μŒκ³Ό 같은 차이점이 μžˆμŠ΅λ‹ˆλ‹€.

  • μ„Έμ…˜ λ ˆλ²¨μ—μ„œμ˜ 읽기 μ „μš© μƒνƒœ: μ„Έμ…˜ λ ˆλ²¨μ—μ„œ 읽기 μ „μš© μƒνƒœλ₯Ό μ„€μ •ν•˜λ©΄, ν•΄λ‹Ή μ„Έμ…˜μ—μ„œ μˆ˜ν–‰λ˜λŠ” λͺ¨λ“  νŠΈλžœμž­μ…˜μ€ 기본적으둜 읽기 μ „μš©μœΌλ‘œ λ™μž‘ν•©λ‹ˆλ‹€. 즉, μ„Έμ…˜ λ‚΄μ˜ λͺ¨λ“  νŠΈλžœμž­μ…˜μ—μ„œ 데이터 λ³€κ²½ μž‘μ—…μ€ κ±°λΆ€λ˜κ±°λ‚˜ λ¬΄μ‹œλ©λ‹ˆλ‹€. 이 섀정은 ν•΄λ‹Ή μ„Έμ…˜ λ‚΄μ˜ λͺ¨λ“  νŠΈλžœμž­μ…˜μ— 적용되며, μ„Έμ…˜μ΄ μ’…λ£Œλ˜κ±°λ‚˜ λͺ…μ‹œμ μœΌλ‘œ λ³€κ²½λ˜κΈ° μ „κΉŒμ§€ μœ μ§€λ©λ‹ˆλ‹€.
  • νŠΈλžœμž­μ…˜ μˆ˜μ€€μ—μ„œμ˜ 읽기 μ „μš© μƒνƒœ: νŠΈλžœμž­μ…˜ μˆ˜μ€€μ—μ„œ 읽기 μ „μš© μƒνƒœλ₯Ό μ„€μ •ν•˜λ©΄, ν•΄λ‹Ή 섀정은 였직 κ·Έ νŠΈλžœμž­μ…˜μ—λ§Œ μ μš©λ©λ‹ˆλ‹€. 즉 ν•΄λ‹Ή νŠΈλžœμž­μ…˜μ΄ μ’…λ£Œλ˜λ©΄, 이 섀정은 ν•΄μ œλ˜λ©° λ‹€μŒ νŠΈλžœμž­μ…˜μ€ μ„Έμ…˜ 레벨의 섀정을 λ”°λ₯΄κ²Œ λ©λ‹ˆλ‹€. μ΄ 섀정은 νŠΈλžœμž­μ…˜μ˜ μ‹œμž‘κ³Ό μ’…λ£Œ μ‚¬μ΄μ—λ§Œ μœ νš¨ν•˜λ©°, νŠΈλžœμž­μ…˜μ˜ λ²”μœ„λ₯Ό λ²—μ–΄λ‚˜λŠ” 영ν–₯은 μ—†μŠ΅λ‹ˆλ‹€.

μ½”λ“œλ₯Ό 보면 "set session transaction " + (readOnlyFlag ? "read only" : "read write") λΆ€λΆ„은 MySQL μ„œλ²„μ— μ „λ‹¬λ˜λŠ” λͺ…λ Ήμ–΄λ‘œ, 5.6.5 이전 λ²„μ „μ—μ„œλŠ” ν•΄λ‹Ή λͺ…λ Ήμ–΄λ₯Ό μ§μ ‘μ μœΌλ‘œ μ§€μ›ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. λ”°λΌμ„œ, MySQL μ„œλ²„μ˜ 버전이 5.6.5 미만일 κ²½μš°μ—λŠ” JDBC λ“œλΌμ΄λ²„κ°€ 'READ ONLY' λ˜λŠ” 'READ WRITE' νŠΈλžœμž­μ…˜ νŠΉμ„±μ„ μ„œλ²„μ— 전달할 수 μ—†μŠ΅λ‹ˆλ‹€.

ν•˜μ§€λ§Œ MySQL μ„œλ²„μ˜ 버전이 5.6.5 미만인 κ²½μš°μ—λ„, μ½”λ“œ λ§ˆμ§€λ§‰μ˜ this.readOnly = readOnlyFlagλ₯Ό 톡해 λ°μ΄ν„°λ² μ΄μŠ€ 연결을 읽기 μ „μš©μœΌλ‘œ μ„€μ •ν•˜λ„λ‘ JDBC λ“œλΌμ΄λ²„μ—κ²Œ μ§€μ‹œν•˜κ²Œ λ©λ‹ˆλ‹€.

결과적으둜 JDBC λ“œλΌμ΄λ²„λŠ” 이 연결을 톡해 λ“€μ–΄μ˜€λŠ” λͺ¨λ“  쿼리λ₯Ό 읽기 μ „μš©μœΌλ‘œ μ·¨κΈ‰ν•˜λ©°, 데이터λ₯Ό λ³€κ²½ν•˜λŠ” 쿼리가 μ‹€ν–‰λ˜λ €κ³  ν•˜λ©΄ JDBC λ“œλΌμ΄λ²„κ°€ 이λ₯Ό κ±°λΆ€ν•˜κ³  였λ₯˜λ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€. 이둜 인해 MySQL μ„œλ²„μ˜ 버전에 상관없이 읽기 μ „μš© νŠΈλžœμž­μ…˜μ˜ μ•ˆμ „μ„±μ€ 보μž₯될 수 μžˆμŠ΅λ‹ˆλ‹€.

DataSourceTransactionManager의 prepareTransactionalConnection()

κ·Έ ν›„ DataSourceTransactionManager 클래슀의 doBegin() λ©”μ„œλ“œ λ‚΄μ—μ„œ DataSourceTransactionManager.prepareTransactionalConnection() λ©”μ„œλ“œκ°€ ν˜ΈμΆœλ©λ‹ˆλ‹€. 이 λ©”μ„œλ“œλŠ” νŠΈλžœμž­μ…˜ μˆ˜μ€€μ—μ„œ 읽기 μ „μš© λͺ¨λ“œλ₯Ό κ°•μ œν•˜κΈ° μœ„ν•œ κ²ƒμž…λ‹ˆλ‹€. 이미 DataSourceUtils.prepareConnectionForTransaction(con, definition) ν˜ΈμΆœμ— μ˜ν•΄ λ°μ΄ν„°λ² μ΄μŠ€ 연결이 읽기 μ „μš©μœΌλ‘œ μ„€μ •λ˜μ—ˆμ§€λ§Œ, μ΄λŠ” μ—°κ²° μˆ˜μ€€μ˜ μ„€μ •μ΄λ―€λ‘œ 이후에 같은 연결을 λ‹€λ₯Έ νŠΈλžœμž­μ…˜μ—μ„œ μž¬μ‚¬μš©ν•  κ²½μš°μ— 읽기 μ „μš© 섀정이 변경될 수 μžˆμŠ΅λ‹ˆλ‹€.

ν•˜μ§€λ§Œ κΈ°λ³Έ μ„€μ • μƒμ—μ„œ DataSourceTransactionManager.prepareTransactionalConnection() λ©”μ„œλ“œκ°€ μ‹€μ œλ‘œ μˆ˜ν–‰λ  κ°€λŠ₯성은 거의 μ—†μŠ΅λ‹ˆλ‹€. isEnforceReadOnly()의 기본값이 false이고, enforceReadOnly 속성은 DataSourceTransactionManager의 μ„€μ •μœΌλ‘œμ¨ @Transactional μ–΄λ…Έν…Œμ΄μ…˜μ˜ readOnly μ†μ„±κ³ΌλŠ” λ³„κ°œμ΄κΈ° λ•Œλ¬Έμž…λ‹ˆλ‹€.

λ”°λΌμ„œ enforceReadOnlyλ₯Ό true둜 μ„€μ •ν•˜λ €λ©΄, DataSourceTransactionManager의 μΈμŠ€ν„΄μŠ€λ₯Ό μƒμ„±ν•˜κ±°λ‚˜ κ΅¬μ„±ν•˜λŠ” κ³Όμ •μ—μ„œ λͺ…μ‹œμ μœΌλ‘œ μ„€μ •ν•΄μ•Ό ν•©λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, μ•„λž˜μ™€ 같이 μ„€μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
    DataSourceTransactionManager txManager = new DataSourceTransactionManager();
    txManager.setDataSource(dataSource);
    txManager.setEnforceReadOnly(true);  // 읽기 μ „μš© νŠΈλžœμž­μ…˜ κ°•μ œ μ„€μ •
    return txManager;
}

μ§€κΈˆκΉŒμ§€μ˜ 과정을 μ •λ¦¬ν•˜λ©΄ λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

  1. DataSourceTransactionManager의 doBegin() λ©”μ„œλ“œκ°€ νŠΈλžœμž­μ…˜μ„ μ‹œμž‘ν•˜λŠ” 과정을 λ‹΄λ‹Ήν•œλ‹€.
  2. doBegin() λ©”μ„œλ“œ λ‚΄λΆ€μ—μ„œ DataSourceUtils.prepareConnectionForTransaction(con, definition)이 ν˜ΈμΆœλ˜λŠ”λ°, 이 ν•¨μˆ˜μ—μ„œλŠ” νŠΈλžœμž­μ…˜ μ •μ˜μ— 따라 λ°μ΄ν„°λ² μ΄μŠ€ 연결을 μ€€λΉ„ν•œλ‹€.
  3. 이 κ³Όμ •μ—μ„œ ConnectionImpl의 setReadOnlyInternal() λ©”μ„œλ“œκ°€ 호좜되며, μ΄λŠ” λ°μ΄ν„°λ² μ΄μŠ€ 연결을 읽기 μ „μš© μƒνƒœλ‘œ μ„€μ •ν•œλ‹€.
  4. λ§ˆμ§€λ§‰μœΌλ‘œ, DataSourceTransactionManager의 doBegin() λ©”μ„œλ“œ λ‚΄μ—μ„œ prepareTransactionalConnection()이 ν˜ΈμΆœλ˜μ–΄, νŠΈλžœμž­μ…˜μ„ μœ„ν•œ μ΅œμ’…μ μΈ λ°μ΄ν„°λ² μ΄μŠ€ μ—°κ²° μ€€λΉ„λ₯Ό μ™„λ£Œν•œλ‹€.

 

4. λΉ„μ¦ˆλ‹ˆμŠ€ 둜직 μ‹€ν–‰

λΉ„μ¦ˆλ‹ˆμŠ€ 둜직이 μ‹€ν–‰λ˜λŠ” 이 λ‹¨κ³„μ—μ„œλŠ” μ• ν”Œλ¦¬μΌ€μ΄μ…˜ λ‘œμ§μ— 따라 λ°μ΄ν„°λ² μ΄μŠ€ 쿼리λ₯Ό μˆ˜ν–‰ν•˜κ²Œ λ©λ‹ˆλ‹€. μ΄λ•Œ λ°μ΄ν„°λ² μ΄μŠ€ 연결이 읽기 μ „μš© λͺ¨λ“œλ‘œ μ„€μ •λ˜μ–΄ μžˆλ‹€λ©΄, 데이터 λ³€κ²½(INSERT, UPDATE, DELETE λ“±)을 μ‹œλ„ν•˜λŠ” SQL λͺ…령이 μ‹€ν–‰λ˜λ©΄ SQLException이 λ°œμƒν•©λ‹ˆλ‹€. μ΄λŸ¬ν•œ 검증 과정은 JDBC λ“œλΌμ΄λ²„κ°€ SQL λͺ…λ Ή μ‹€ν–‰ 전에 ν˜„μž¬ λ°μ΄ν„°λ² μ΄μŠ€ μ—°κ²°μ˜ μƒνƒœλ₯Ό ν™•μΈν•˜μ—¬ μ΄λ£¨μ–΄μ§‘λ‹ˆλ‹€.

MySQL 5.6.5 이상 

MySQL 5.6.5 μ΄μƒμ—μ„œλŠ” JDBC λ“œλΌμ΄λ²„μ™€ MySQL μ„œλ²„ λͺ¨λ‘μ—μ„œ 'READ ONLY' νŠΈλžœμž­μ…˜μ„ κ΄€λ¦¬ν•©λ‹ˆλ‹€. μ΄λ•Œμ˜ λ™μž‘ 과정은 λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

  1. JDBC λ“œλΌμ΄λ²„ μ°¨μ›μ˜ 검증: μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ΄ SQL λͺ…령을 μ‹€ν–‰ν•˜λ €κ³  ν•  λ•Œ, JDBC λ“œλΌμ΄λ²„λŠ” λ¨Όμ € μ—°κ²°μ˜ μƒνƒœλ₯Ό ν™•μΈν•©λ‹ˆλ‹€. λ§Œμ•½ 연결이 읽기 μ „μš© λͺ¨λ“œλ‘œ μ„€μ •λ˜μ–΄ μžˆλ‹€λ©΄, JDBC λ“œλΌμ΄λ²„λŠ” 데이터 변경을 μ‹œλ„ν•˜λŠ” SQL λͺ…령을 κ±°λΆ€ν•˜κ³  SQLException을 λ°œμƒμ‹œν‚΅λ‹ˆλ‹€. 
  2. MySQL μ„œλ²„ μ°¨μ›μ˜ 검증: λ§Œμ•½ JDBC λ“œλΌμ΄λ²„ μ°¨μ›μ—μ„œ SQL λͺ…령이 κ±°λΆ€λ˜μ§€ μ•Šμ•˜λ‹€λ©΄, SQL λͺ…령은 MySQL μ„œλ²„λ‘œ μ „λ‹¬λ©λ‹ˆλ‹€. MySQL μ„œλ²„λŠ” 'READ ONLY' νŠΈλžœμž­μ…˜ 섀정을 μΈμ‹ν•˜κ³ , 데이터 변경을 μ‹œλ„ν•˜λŠ” SQL λͺ…령을 μ°¨λ‹¨ν•˜κ³  였λ₯˜λ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€. 

MySQL 5.6.5 미만

MySQL 5.6.5 λ―Έλ§Œμ—μ„œλŠ” 'READ ONLY'와 'READ WRITE' νŠΈλžœμž­μ…˜ νŠΉμ„±μ΄ μ„œλ²„ μ°¨μ›μ—μ„œ μ§€μ›λ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. 이 경우 검증 과정은 JDBC λ“œλΌμ΄λ²„κ°€ μ „μ μœΌλ‘œ λ‹΄λ‹Ήν•©λ‹ˆλ‹€. λ§ˆμ°¬κ°€μ§€λ‘œ JDBC λ“œλΌμ΄λ²„λŠ” λ¨Όμ € μ—°κ²°μ˜ μƒνƒœλ₯Ό ν™•μΈν•˜κ³ , 연결이 읽기 μ „μš© λͺ¨λ“œλ‘œ μ„€μ •λ˜μ–΄ μžˆλŠ”λ° 데이터 변경을 μ‹œλ„ν•˜λŠ” SQL λͺ…령이 λ“€μ–΄μ˜¨λ‹€λ©΄ SQLException을 λ°œμƒμ‹œν‚΅λ‹ˆλ‹€.
 

5. νŠΈλžœμž­μ…˜ μ’…λ£Œ

νŠΈλžœμž­μ…˜ μ’…λ£Œ λ‹¨κ³„λŠ” λΉ„μ¦ˆλ‹ˆμŠ€ 둜직 싀행이 μ™„λ£Œλœ 후에 이루어지며, 이 λ‹¨κ³„μ—μ„œ 컀밋 ν˜Ήμ€ 둀백이 μΌμ–΄λ‚˜κ²Œ λ©λ‹ˆλ‹€.

 

 

정리

읽기 μ „μš© νŠΈλžœμž­μ…˜μ€ λ°μ΄ν„°λ² μ΄μŠ€ μž‘μ—…μ—μ„œ λ°μ΄ν„°μ˜ 일관성을 μœ μ§€ν•˜λ©° μ„±λŠ₯을 ν–₯μƒμ‹œν‚€λŠ” μ€‘μš”ν•œ λ„κ΅¬μž…λ‹ˆλ‹€. 특히 데이터λ₯Ό μ½λŠ” μž‘μ—…μ΄ μ£Όλ₯Ό μ΄λ£¨λŠ” ν™˜κ²½μ—μ„œλŠ” 읽기 μ „μš© νŠΈλžœμž­μ…˜μ˜ ν™œμš©μ΄ 큰 이점을 κ°€μ Έμ˜¬ 수 μžˆμŠ΅λ‹ˆλ‹€.

읽기 μ „μš© νŠΈλžœμž­μ…˜μ˜ μ„€μ •, λΉ„μ¦ˆλ‹ˆμŠ€ 둜직의 μ‹€ν–‰, 그리고 νŠΈλžœμž­μ…˜μ˜ μ’…λ£Œμ˜ μ„Έ 단계λ₯Ό 거치며 이 κ³Όμ •μ—μ„œ λ°μ΄ν„°λ² μ΄μŠ€ μ—°κ²°μ˜ μƒνƒœλ₯Ό ν™•μΈν•˜κ³ , ν•„μš”μ— 따라 컀밋 ν˜Ήμ€ 둀백을 μˆ˜ν–‰ν•˜κ²Œ λ©λ‹ˆλ‹€. MySQL 5.6.5 이상과 미만의 λ²„μ „μ—μ„œλŠ” 'READ ONLY' νŠΈλžœμž­μ…˜μ˜ 관리 방식이 μ•½κ°„ λ‹€λ₯΄μ§€λ§Œ 핡심적인 뢀뢄은 λ™μΌν•©λ‹ˆλ‹€. 즉 μ–΄λŠ λ²„μ „μ—μ„œλ“  읽기 μ „μš© λͺ¨λ“œλ‘œ μ„€μ •λœ μ—°κ²°μ—μ„œ 데이터 변경을 μ‹œλ„ν•˜λ©΄ SQLException이 λ°œμƒν•©λ‹ˆλ‹€. 

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

λŒ“κΈ€