락 (Lock)
- 물리적으로 특정 행(row) 또는 테이블(table)에 접근 제어를 거는 방법.
- 다른 트랜잭션이 접근하지 못하게 막는 매커니즘.
비관적 락(Pessimistic Lock)
SELECT FOR UPDATE ...
비관적 락은 데이터베이스 락 매커니즘에 의존하는 동시성 제어 방법으로, select for update 쿼리가 수행되는 것이라고 생각하시면 된다. 먼저 자원에 접근한 트랜잭션이 락을 획득하게 되고 다른 트랜잭션은 락을 획득하기 전까지 대기한다.
낙관적 락(Optimistic Lock)
- 충돌이 일어나지 않을 것이라고 판단
- 물리적인 락을 사용하지 않음.
- version 필드 사용
- 장점
- 락 대기 문제 회피
- 데이터를 읽는 시점에 대응하지 않고 쓰는 시점에 대응하기 때문에 다른 트랜잭션에 영향을 주지 않음.
- 락 대기 문제 회피
- 단점
- 충돌이 잦은 경우 성능 저하
- 비관적 락이 대기하는 것과 다르게 충돌이 일어날 때마다 재시도를 해야하는 상황이 발생
- 충돌이 잦은 경우 성능 저하
즉, 비관적 락은 동시성 문제가 자주 발생하는 곳, 데이터의 일관성의 중요도가 큰 곳 (결제, 수강 신청...) 에 사용되고
낙관적 락은 동시성 문제가 발생하지만 매우 드물거나 간단한 작업일 경우 적합하다.
예제로 약 100명이 동시에 하나의 수업에 좋아요를 누른다고 가정해보자.
1. 락을 사용하지 않는 경우
/**
* 락 없이 사용
*/
@Transactional
public void callWithoutLock(Long courseId) {
courseService.updateCourseLike(courseId);
}
그냥 똑같이 쓰면 된다.
2. 비관적 락 사용
[repository]
/**
* 비관적 락 사용
*/
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT c FROM Course c WHERE c.id = :id")
Optional<Course> findByIdWithPessimisticLock(@Param("id") Long id);
@Lock(LockModeType.PESSIMISTIC_WRITE) 이 추가되었다.
[service]
/**
* 비관적 락 사용
*/
@Transactional
public void callPessimistic(Long courseId) {
// 1. 수업 엔티티 조회
Course foundCourse = courseRepository.findByIdWithPessimisticLock(courseId)
.orElseThrow(RuntimeException::new);
// 2. like 증가
foundCourse.increaseLike();
}
3. 낙관적 락 사용
[course entity]
@Entity
@Getter
@Table(name = "course")
public class Course {
...
// 버전 명시 필요
@Version
private Long version;
...
}
낙관적 락은 version으로 변경을 커밋할지 안 할지 결정하기 때문에 @Version 어노테이션을 붙인 변수가 필요하다.
[service]
/**
* 낙관적 락 사용
*/
public int callOptimistic(Long courseId) {
boolean success = false;
int failureCount = 0; // 실패 횟수 기록
while (!success) {
try {
// 1. 로직 수행: 좋아요 증가
courseService.updateCourseLike(courseId);
// 2. 성공처리
success = true;
} catch (ObjectOptimisticLockingFailureException e) {
failureCount++;
}
}
return failureCount;
}
엄청나게 많은 실패 횟수가 찍히는 것을 볼 수 있다.
[테스트 코드]
/**
* 락없이 테스트
* 테스트시 Course 엔티티에서 Version 주석 필요
*/
@Test
@DisplayName("락없이 테스트")
public void testWithoutLock() throws InterruptedException {
// given
Course newCourse = Course.createNewCourse("noLockCourse");
Course savedCourse = courseRepository.save(newCourse);
// when
ExecutorService executorService = Executors.newFixedThreadPool(10);
CountDownLatch latch = new CountDownLatch(100);
for (int i = 0; i < 100; i++) {
executorService.execute(() -> {
try {
concurrencyService.callWithoutLock(savedCourse.getId());
} finally {
latch.countDown();
}
});
}
latch.await();
executorService.shutdown();
// then
Course updatedCourse = courseRepository.findById(savedCourse.getId()).get();
System.out.println("Final like count (no lock): " + updatedCourse.getLikeCnt());
}
/**
* 비관락 사용
*/
@Test
@DisplayName("비관락 테스트")
public void testPessimistic() throws InterruptedException {
// given
Course newCourse = Course.createNewCourse("pessimisticCourse");
Course savedCourse = courseRepository.save(newCourse);
// when
ExecutorService executorService = Executors.newFixedThreadPool(10);
CountDownLatch latch = new CountDownLatch(100);
for (int i = 0; i < 100; i++) {
executorService.execute(() -> {
try {
concurrencyService.callPessimistic(savedCourse.getId());
} finally {
latch.countDown();
}
});
}
latch.await();
executorService.shutdown();
// then
Course updatedCourse = courseRepository.findById(savedCourse.getId()).get();
System.out.println("Final like count (pessimistic): " + updatedCourse.getLikeCnt());
}
/**
* 낙관락 테스트
* 테스트시 Course 엔티티에서 Version 주석 활성화 필요.
*/
@Test
@DisplayName("낙관락 테스트")
public void testOptimistic() throws InterruptedException {
// given
Course newCourse = Course.createNewCourse("optimistic");
Course savedCourse = courseRepository.save(newCourse);
AtomicInteger totalFailures = new AtomicInteger(0);
// when
ExecutorService executorService = Executors.newFixedThreadPool(10);
CountDownLatch latch = new CountDownLatch(100);
for (int i = 0; i < 100; i++) {
executorService.execute(() -> {
try {
int attempts = concurrencyService.callOptimistic(savedCourse.getId());
totalFailures.addAndGet(attempts);
} finally {
latch.countDown();
}
});
}
latch.await();
executorService.shutdown();
// then
Course updatedCourse = courseRepository.findById(savedCourse.getId()).get();
System.out.println("Total attempts: " + totalFailures.get());
System.out.println("Final like count (optimistic): " + updatedCourse.getLikeCnt());
}
}
'내배캠 > TIL' 카테고리의 다른 글
[스프링] @SpringBootTest 관련 트러블슈팅 (2) | 2024.10.10 |
---|---|
[AWS] RDS 생성 및 연동하기 (4) | 2024.10.09 |
[스프링] JWT를 이용한 Spring Security 구현 (문제 & 해결) (1) | 2024.10.05 |
[스프링] 트랜잭션 활용과 주의사항 (1) | 2024.10.01 |
[AWS] Identity and Access Management (0) | 2024.09.30 |