들어가기 전 

이번 포스팅에서는 비관적락과 낙관적락에 대해서 알아보겠습니다.

알아보기 전 공유 락, 배타적 락, 격리 수준에 대해서 모르시는 분은 아래 포스팅을 읽고 이어서 진행하시는 것을 추천드리겠습니다.

 

https://hoestory.tistory.com/87

 

[MySQL] 데드락 및 데이터베이스 Lock(Shared Lock, Exclusive Lock, Record Lock)에 대하여 -1

들어가기 전 이번 포스팅에서는 데드락과 데이터베이스 Lock에 대해서 알아보겠습니다.트랜잭션 격리 수준에 대한 내용은 아래 포스팅을 참고하시면 됩니다. https://hoestory.tistory.com/86 [MySQL] 트

hoestory.tistory.com

 

https://hoestory.tistory.com/86

 

[MySQL] 트랜잭션 격리 수준

들어가기 전 이번 포스팅에서는 데이터베이스 트랜잭션의 격리 수준에 대해서 알아보겠습니다. 트랜잭션 격리 수준트랜잭션 격리 수준은 여러 트랜잭션이 동시에 실행되는 상황에서 특정 트

hoestory.tistory.com

 

 

비관적 락(Pessmistic Lock)

 

비관적 락은 트랜잭션에 진입하게 되면 공유락(Shared Lock) 또는 배타적락(Exclusive Lock)을 걸어서 다른 트랜잭션에서 진입하지 못하게 차단합니다.

비관적락의 격리 수준은 Repeatable Read 또는 Serializable입니다.

다른 트랜잭션에서 진입하지 못하기 때문에 데이터 일관성을 유지할 수 있고 트랜잭션 충돌이 발생할 일이 적습니다.

하지만 이러한 특성 때문에 락을 걸고 해제하는 과정에서 오버헤드가 발생할 수 있습니다.

그리고 다른 트랜잭션의 접근을 차단하기 때문에 병목현상이 발생할 수 있습니다.

 

 

하나의 트랜잭션에서 Exclusive Lock을 획득하여 다른 트랜잭션의 진입을 차단을 하였습니다.

Exlcusive Lock을 획득하고 있던 트랜잭션이 커밋을 하고 Lock을 해제하여야 다른 트랜잭션에서 접근할 수 있습니다.

 

낙관적 락(Optimistic Lock)

 

낙관적 락은 비관적 락과 달리 트랜잭션이 완료될 때 충돌이 발생하는지를 확인하는 방식입니다.

version, hashcode 또는 timestamp를 이용하여 충돌을 방지합니다.

낙관적락은 비관적 락과 달리 트랜잭션 진입을 차단하지 않기 때문에 동시성이 뛰어나고 데드락 발생 가능성이 낮습니다.

하지만 충돌이 발생했을 때 데이터를 다시 읽고 업데이트를 해야 하는 추가작업이 필요합니다.

 

 

Client A 트랜잭션과 B 트랜잭션에 특정 데이터를 조회할 때 Version은 1의 값입니다.

B 트랜잭션에서 데이터 갱신을 한 뒤 Commit을 하여 Version의 값이 2로 증가한 상태에서 A 트랜잭션에서도 데이터 갱신을 시도하려다가 실패를 하였습니다.

이유는 A 트랜잭션의 Version의 값은 1이었으나 B 트랜잭션에서 데이터를 갱신하여 Version의 값이 2가 되어 조회했던 시점의 Version의 값과 커밋하는 시점에 Version의 값이 달라 충돌이 일어나서 데이터 갱신에 실패하는 것입니다.

 

지금까지 이론으로 낙관적 락과 비관적락이 무엇인지 알아보았습니다.
아래 예제에서는 낙관적 락에 대해서 알아보겠습니다.
(참고 : 비관적 락에 대한 예시는 아래 포스팅을 참고하시면 좋을 거 같습니다.)
https://hoestory.tistory.com/87

 

낙관적 락

낙관적 락 예제에서 JPA에서 제공해 주는 @Version을 활용해서 예제를 설명하겠습니다.

 

import jakarta.persistence.Version;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OptimisticLock {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @Version
    private Integer version;

    public OptimisticLock(String name) {
        this.name = name;
    }

    public void updateName(String name) {
        this.name = name;
    }
}

 

  • @Version : 데이터가 갱신될 때 version의 값도 자동으로 증가합니다.

 

@Service
@Transactional
@RequiredArgsConstructor
public class OptimisticLockService {
    private final OptimisticLockRepository optimisticLockRepository;

    public void update(Long id, String updateName) {
        OptimisticLock optimisticLock = optimisticLockRepository.findById(id).get();
        optimisticLock.updateName(updateName);
    }
}

 

 

@SpringBootTest
class OptimisticLockServiceTest {
    @Autowired
    private OptimisticLockService optimisticLockService;
    @Autowired
    private OptimisticLockRepository optimisticLockRepository;

    @BeforeEach
    void save() {
        optimisticLockRepository.save(new OptimisticLock("테스트1"));
        optimisticLockRepository.save(new OptimisticLock("테스트2"));
    }

    @DisplayName("낙관적 락 발생")
    @Test
    void occurOptimisticLock() throws InterruptedException {
        //given
        int threadCount = 2;
        CountDownLatch countDownLatch = new CountDownLatch(threadCount);
        ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
        for(int i = 0; i < threadCount; i++) {
            executorService.execute(() -> {
                    optimisticLockService.update(1L, "테스트");
                    System.out.println("실행됨");
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        Thread.sleep(500);
    }
}

 

위 테스트 코드는 두개의 스레드가 동시에 데이터를 갱신을 수행하는 코드입니다.

위와 같이 테스트 코드를 실행시키면 아래와 같은 낙관적 락에 대한 예외가 발생합니다.

 

Exception in thread "pool-2-thread-2" org.springframework.orm.ObjectOptimisticLockingFailureException:
Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : 
[transaction.propagation.domain.entity.OptimisticLock#1]

 

이 예외는 테스트 코드에서 여러 트랜잭션이 동시에 하나의 데이터를 갱신하려고 접근하면서 발생합니다. 한 트랜잭션이 데이터 갱신을 완료하고 커밋을 하면, 해당 데이터의 Version 값이 증가하게 됩니다. 그러나 아직 커밋되지 않은 다른 트랜잭션은 이전의 Version 값을 참조하고 있으므로, 현재 데이터의 Version 값과 일치하지 않아 낙관적 락 충돌이 발생하며 위와 같은 에러가 나타납니다.