스프링에서 동시성 문제를 해결하기 위해 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개로 정상 실행된다.
낙관적 락,비관적 락 모두 트랜잭션 충돌에 대해 방어 할 수 있어 동시성문제를 해결할 수 있다. 하지만 낙관적락은 충돌이 나면 재시도를 하지 않고,비관적락은 락이 풀릴때 까지 대기한다는 점이 차이가 크다.