낙관적 락,비관적 락 (Spring)

Choco·2024년 4월 30일
1
post-thumbnail

스프링에서 동시성 문제를 해결하기 위해 Lock을 건다. 어떤 방식으로 사용되는지 알아보자.

Lock

낙관적 락

-> 트랜잭션이 충돌하지 않는다고 가정
-> 실제 DB에 락을 걸지는 않고 version 확인만 한다
-> 충돌이 되면 조치를 취한다
-> 충돌 하더라도 기다리지않고 에러를 반환한다

@Entity
@Getter
@NoArgsConstructor
public class Dataset {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long datasetId;
    
	private Integer view;
    
    @Version
    private Integer version;
}

Entitiy 필드에 @Version을 통해 선언한다.

작동 방식

트랜잭션 A,B가 있다고 하자
트랜잭션 A,B 현재 조회수를 뜻하는 view에 +1을 하여 수정 하려고 한다.

A가 먼저 트랜잭션을 시작하고 동시에 B가 트랜잭션을 시작했다고 하자.
현재 view의 버전은 1이므로 둘다 1일때 수정을 한다.
A 트랜잭션이 커밋되어 버전이 2로 올라갔다.
B 트랜잭션은 커밋될때 버전이 맞지 않으므로 롤백이 된다.

테스트

5개의 스레드를 이용하여 10명의 사용자가 동시에 조회수를 올린다고 가정하자.

@Test
@DisplayName("동시성 문제")
void raceConditionView() throws InterruptedException {
    //Given
    Dataset dataset = datasetRepository.save(Dataset.builder().view(0).build());

    //When

    //스레드 풀 5개로 설정
    ExecutorService executorService = Executors.newFixedThreadPool(5);
    //10명의 사용자
    CountDownLatch latch = new CountDownLatch(10);
    for (int i = 0; i < 10; i++) {
        executorService.execute(() -> {
            datasetService.getDatasetDetail(dataset.getDatasetId());
            latch.countDown();
        });
    }
    latch.await();
    executorService.shutdown();

    Assertions.assertThat(datasetRepository.findById(dataset.getDatasetId()).get().getView()).isEqualTo(10);
    
   	//Then
    System.out.println("현재 조회 수:"+datasetRepository.findById(dataset.getDatasetId()).get().getView());
}

실행시 ObjectOptimisticLockingFailureException 예외가 발생한다.
이는 한 트랜잭션이 Version이 다른 엔티티를 수정하려고 커밋 하였을때 발생한다.

사용하면 좋을 상황

비관적 락

-> 트랜잭션 충돌이 발생한다고 가정
-> 트랜잭션 커밋전 락을 한다.
-> 동시에 실행되는 다른 트랜잭션은 락이 걸린 부분에 대해 대기하였다가 실행한다.

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select d from Dataset d left join fetch d.resource left join fetch d.datasetThemeList where d.datasetId = :datasetId")
Optional<Dataset> findByIdWithResourceAndTheme(Long datasetId);

Repository에 @Lock(LockModeType.PESSIMISTIC_WRITE)을 통해 선언한다.
-> select ~ for update문이 나간다.
@Lock(LockModeType.PESSIMISTIC_READ)로 s-lock도 수행 가능하다.
-> select ~ share문이 나간다.

테스트

위와 똑같은 코드로 테스트를 할 시

@Test
@DisplayName("동시성 문제")
void raceConditionView() throws InterruptedException {
    //Given
    Dataset dataset = datasetRepository.save(Dataset.builder().view(0).build());

    //When

    //스레드 풀 5개로 설정
    ExecutorService executorService = Executors.newFixedThreadPool(5);
    //10명의 사용자
    CountDownLatch latch = new CountDownLatch(10);
    for (int i = 0; i < 10; i++) {
        executorService.execute(() -> {
            datasetService.getDatasetDetail(dataset.getDatasetId());
            latch.countDown();
        });
    }
    latch.await();
    executorService.shutdown();

    Assertions.assertThat(datasetRepository.findById(dataset.getDatasetId()).get().getView()).isEqualTo(10);
    
   	//Then
    System.out.println("현재 조회 수:"+datasetRepository.findById(dataset.getDatasetId()).get().getView());
}

현재 조회수는 10개로 정상 실행된다.

결론

낙관적 락,비관적 락 모두 트랜잭션 충돌에 대해 방어 할 수 있어 동시성문제를 해결할 수 있다. 하지만 낙관적락은 충돌이 나면 재시도를 하지 않고,비관적락은 락이 풀릴때 까지 대기한다는 점이 차이가 크다.

profile
주니어 백엔드 개발자 입니다:)

0개의 댓글