Database

낙관적 락과 REPEATABLE READ

mysterious dev 2025. 2. 24. 22:45


고민

JPA의 낙관적 락(Optimistic Lock)은 트랜잭션 커밋 시점에 데이터베이스의 version 값을 확인하여, 
최초 조회 시점의 version 값과 다를 경우 롤백하는 방식으로 동작한다.

 

이때 다음과 같은 의문점이 들었다.

commit 시점에 version 을 확인하여 최초 조회 시점과 달라진 경우 예외를 발생시키도록 동작한다면,
version 을 확인하기 위해서는 기존 트랜잭션이 아닌 새로운 트랜잭션을 만든 후,
새롭게 만들어진 트랜잭션을 통해 version 값을 확인해오는 식으로 동작하는건가?



이와 같이 생각한 이유는 다음과 같다.

  • MYSQL 의 기본 isolation level 은 REPEATABLE READ 로써, 이는 한 트랜잭션에서 동일한 값을 여러번 읽었을 때, 그 결과가 동일함을 보장해준다.
  • 낙관적 락 사용 시, 트랜잭션 내에서 특정 데이터를 읽어온 경우, commit 직전에 같은 트랜잭션에서 읽더라도 version 값은 최초 읽은 값과 동일하게 읽혀질 것인데, 이렇게 되면 낙관적 락이 동작하지 않게 된다.
  • 따라서 version을 확인하기 위해 새로운 트랜잭션을 만든 후, 해당 트랜잭션을 통해 version 값을 읽어와 비교하는 식으로 동작할 것이라고 판단했다.

 

 

결론

결론적으로는 그렇지 않고, UPDATE 시 WHERE 절에 'version = 최초읽은시점의 version 값' 조건을 주어, 영향을 받은 row 가 0 이면 예외를 발생시키는 식으로 동작한다.
이때 UPDATE 절의 WHERE 조건은 MVCC 로 인해 만들어진 undo 로그를 보는 것이 아닌, 실제 데이터를 보는 식으로 동작한다. 
(참고 - update 쿼리 시에는 x-lock 이 걸리는데, mysql 의 Undo log 에는 lock 이 걸리지 않는다. 실제 데이터에 락이 걸림)

 

 

 

 

검증 - MySQL 에서 UPDATE의 동작

환경
- DB: MySQL 8.0 버전
- isolation level: REPEATABLE READ


먼저 MySQL 을 직접 사용하여, update 의 동작을 확인하자.

CREATE TABLE member  
(  
    id      bigint              not null primary key auto_increment,  
    name    varchar(255) unique not null,  
    age     int                 not null,  
    version bigint              not null default 0  
);

INSERT member (name, age) values ('A', 10);  
INSERT member (name, age) values ('B', 20);  
INSERT member (name, age) values ('C', 30);


TX 1

set autocommit = 0;  
  
# 1  
SELECT * FROM member WHERE name = 'A';  
  
  
  
  
# 5  
SELECT * FROM member WHERE name = 'A';  
  
# 6  
UPDATE member SET age = 11, version = 1 WHERE name='A' AND version = 0;  
SELECT * FROM member WHERE name = 'A';
  
# 7  
commit ;


TX 2

set autocommit = 0;  
  
# 2  
SELECT * FROM member WHERE name = 'A';  
  
# 3  
UPDATE member SET age = 2, version = 1 WHERE name='A' AND version = 0;  
SELECT * FROM member WHERE name = 'A';
  
# 4  
commit;
  • # 1의 결과

 

  • # 2의 결과

 

  • # 3의 결과

 

 

  • # 4의 결과 -> commit

 

  • # 5의 결과

조회의 경우 version 0, age 는 10으로 되는 모습

 

  • # 6의 update 쿼리 실행 결과

affected row 가 0개

update 실행 결과 affected row 가 없는 것을 확인할 수 있으며, SELECT 의 결과를 통해 확인하더라도 값이 바뀌지 않음을 확인할 수 있음.

 

 

 

만약 TX 1 의 UPDATE 쿼리의 WHERE 조건이 version=1 로 변경된다면?

set autocommit = 0;  
  
# 1  
SELECT * FROM member WHERE name = 'A';  
  
  
# 5  
SELECT * FROM member WHERE name = 'A';  
  
# 6  
UPDATE member SET age = 11, version = 1 WHERE name='A' AND version = 1;  
SELECT * FROM member WHERE name = 'A';  
  
# 7  
commit ;
  • # 5 까지는 동일

 

 

  • # 6의 Update 실행 시

affected row 가 1개

 

  • 6의 select 실행 시

 

이를 통해 알 수 있듯, update 시에는 undo log 의 값을 보지 않고, 실제 데이터를 확인함!

(참고 - MySQL 에서는 Undo log 에 lock 을 걸지 않는다. 즉 UPDATE 쿼리 시 x-lock 은 undo log 가 아닌 실제 데이터를 잠금)

 

 

 

검증 - JPA 의 낙관적 락 동작

환경
- Java: 21
- Spring Boot : 3.4.3
  • (... 생략)
  • JdbcResourceLocalTransactionCoordinatorImpl.commit()
  • JdbcResourceLocalTransactionCoordinatorImpl.beforeCompletionCallback()
  • TransactionCoordinatorOwner.beforeTransactionCompletion()
    • 구현체: JdbcCoordinatorImpl
  • (JdbcSessionOwner)owner.beforeTransactionCompletion();
  • 구현체: SessionImpl
  • SessionImpl.flushBeforeTransactionCompletion()
  • SessionImpl.managedFlush()
  • SessionImpl.doFlush()
  • fastSessionServices.eventListenerGroup_FLUSH .fireEventOnEachListener( event, FlushEventListener::onFlush );
  • DefaultFlushEventListener.onFlush()
  • DefaultFlushEventListener.performExecutions()
  • ActionQueue.executeActions();
  • EntityUpdateAction.executeActions();
  • EntityUpdateAction.execute();
  • persister.getUpdateCoordinator().update();
  • UpdateCoordinatorStandard.update()
  • UpdateCoordinatorStandard.performUpdate()
  • UpdateCoordinatorStandard.doStaticUpdate()
  • mutationExecutor.execute()
  • mutationExecutor : MutationExecutorSingleNonBatched
  • MutationExecutorSingleNonBatched.performNonBatchedOperations()
  • AbstractMutationExecutor.performNonBatchedMutation()
  • ModelMutationHelper.checkResults();
  • UpdateCoordinatorStandard.checkResult()
  • ModelMutationHelper.identifiedResultsCheck()
  • statementDetails.getExpectation().verifyOutcome() 에서 체크!