낙관적 락 & 비관적 락

2024. 2. 5. 11:32
728x90

: 데이터베이스에 다수의 사용자가 동시에 접근하는 경우 적절히 통제하지 않으면 데이터 무결성에 문제가 발생할 수 있다...

>> 데이터베이스 자원을 잠궈서 읽거나 쓰는 것을 방지하자!

 

1. 낙관적 락 (Optimistic Lock)

: 데이터를 갱신해야 하는 시점에 데이터가 처음 조회했을 때의 시점에서 변화하지 않았다는 것을 바탕으로 실행될지 안될지를 결정한다. 

- 실제로 데이터베이스의 자원을 잠그지는 않음.

1) 트랜잭션 시작에 갱신하고 싶은 데이터 먼저 확인

2) 데이터 수정을 위한 작업 진행

3) 커밋 직전, 현재 상태와 처음 데이터를 불러온 상태의 특정 컬럼 비교

4) 변경이 없다면 그대로 커밋, 변경이 있다면 충돌 상황을 조치. 

- 변경사항이 생겼다는 사실을 기록할 컬럼이 필요!

- 접근 자체를 제어하지 않기 때문에 성능 저하가 적다.

- 그러나, 충돌이 발생하면 충돌 상황을 직접 해소해야 함. 

- 충돌이 자주 발생하지 않을 상황에서 주로 활용.

 

- @Version 어노테이션 활용 : 동시 수정시 OptimisticLockException 발생. 

@Entity
public class Item {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String description;
    private Integer price;
    private Integer stock;

    @Version
    private Long version;

    // ...
}

- Test Code

    @Test
    public void optimisticLock() throws InterruptedException {
        // given
        itemRepository.save(Item.builder()
                .stock(25)
                .build());

        // when
        // 몇번의 동시 요청이 있을것인지
        int threads = 3;
        // 멀티쓰레드를 실행하기 위한 실행자
        ExecutorService executorService
                = Executors.newFixedThreadPool(threads);
        // 결과를 담기위한 리스트
        List<Future<?>> futures = new ArrayList<>();
        for (int i = 0; i < threads; i++) {
            // 여러개의 요청을 보낼 준비
            futures.add(executorService.submit(
                    () -> shopService.decreaseStockOpt()
            ));
        }

        // then
        Exception result = new Exception();
        try {
            for (Future<?> future: futures)
                future.get();
        } catch (ExecutionException e) {
            result = (Exception) e.getCause();
        }

        assertTrue(result instanceof OptimisticLockingFailureException);
    }

 

2. 비관적 락 (Pessimistic Lock)

: 서로 다른 트랜잭션의 접근을 막고, 순차적으로 접근하도록 자원을 잠근다. 

- 접근 방지로 충돌이 미연에 방지되어 데이터 무결성이 잘 지켜진다. 

- 충돌이 일어나지 않아도 Lock이 진행되어서 성능이 저하된다. 

- 서로 다른 트랜잭션이 상대방의 Lock이 해제되길 기다리는 교착상태(Deadlock)가 발생할 위험성이 존재.

- 트랜잭션 내부에서 Lock을 진행하면 트랜잭션이 종료될 때까지 유지됨.

 

1) Shared Lock

: 여러 트랜잭션에서 읽기는 가능하지만, 쓰기는 불가능하게 함. (읽기 가능)

2) Exclusive Lock

: 하나의 트랜잭션에서만 읽고 쓰기가 가능하게 함.  (읽기, 쓰기 불가능)

 

- MySQL 기준

--Shared Lock
SELECT * 
FROM instructor
FOR SHARE;

--Exclusive Lock
SELECT * 
FROM instructor
FOR UPDATE;

 

- Spring Data JPA 기준

: @Lock 어노테이션 추가

  • LockModeType.PESSIMISTIC_READ
  • LockModeType.PESSIMISTIC_WRITE
@Lock(LockModeType.PESSIMISTIC_READ) // 비관적 읽기 락
@Query("SELECT i FROM Item i WHERE i.id = :id")
Optional<Item> findItemForShare(
        @Param("id")
        Long id
);

@Lock(LockModeType.PESSIMISTIC_WRITE) // 비관적 쓰기 락
@Query("SELECT i FROM Item i WHERE i.id = :id")
Optional<Item> findItemForUpdate(
        @Param("id")
        Long id
);

// CrudRepository에 정의된 메서드를 오버라이드 가능
@Override
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<Item> findById(Long id);


    @Transactional
    public void decreaseStockShare() {
        Item item = itemRepository.findItemForShare(1L).orElseThrow();
        item.setStock(item.getStock() - 10);
        itemRepository.save(item);
    }

    @Transactional
    public void decreaseStockUpdate() {
        Item item = itemRepository.findItemForUpdate(1L).orElseThrow();
        item.setStock(item.getStock() - 10);
        itemRepository.save(item);
    }

 

- Test Code

    @Test
    public void pessimisticLock() throws InterruptedException {
        // given
        itemRepository.save(Item.builder()
                .stock(55)
                .build());

        // when
        // 몇번의 동시 요청이 있을것인지
        int threads = 5;
        // 멀티쓰레드를 실행하기 위한 실행자
        ExecutorService executorService
                = Executors.newFixedThreadPool(threads);
        // 결과를 담기위한 리스트
        List<Future<?>> futures = new ArrayList<>();
        for (int i = 0; i < threads; i++) {
            // 여러개의 요청을 보낼 준비
            futures.add(executorService.submit(
                    () -> shopService.decreaseStockShare()
            ));
        }

        // then
        Exception result = new Exception();
        try {
            for (Future<?> future: futures)
                future.get();
        } catch (ExecutionException e) {
            result = (Exception) e.getCause();
        }

        System.out.println(result.getClass());
        assertTrue(result instanceof OptimisticLockingFailureException);
        Item item = itemRepository.findById(1L).get();
        assertEquals(5, item.getStock());
    }

 

728x90

'Programming > Spring, SpringBoot' 카테고리의 다른 글

Querydsl  (1) 2024.02.06
[JPA] N+1  (0) 2024.02.05
[JPA] 영속성 컨텍스트 (Persistence Context)  (0) 2024.02.05
@Query  (1) 2024.02.01
Relations (M : N 관계)  (1) 2024.02.01

BELATED ARTICLES

more