한정 수량이 정해진 “핫딜”이 등록되면 유저가 정해진 수량 이내에 구매가 가능하다
(아직 구매 개수의 제한에 대한 논의는 이루어지지 않았음)
심각한 그림판
동시 다발적으로 유저가 한 가지 핫딜 품목을 구매하려 했을 때 A유저의 구매 트랜잭션이 커밋 되지 않은 상태에서 B유저가 구매 행위를 했을 때 한정 수량을 넘겨서 판매하거나 한정 수량이 음수값이 되는 등 데이터 정합성이 깨지는 문제가 있다.
이런 문제를 레이스 컨디션
이라고 부른다. 위에 그림으로만 봐도 A의 처리 이후 잔고는 “5”로 업데이트를 하겠지만 B의 트랜잭션 이후에는 “3”으로 업데이트를 하려고 할것이다.
레이스 컨디션의 결과:
데이터 손상
Race Condition으로 인해 데이터 Condition이 일관되지 않거나 잘못될 수 있다.
예측할 수 없는 동작
Race Condition의 비결정적 특성으로 인해 시스템 동작을 예측하거나 재현하기 어려울 수 있다.
보안 취약성
Race Condition은 때때로 공격자가 무단 액세스 또는 권한을 얻기 위해 악용할 수 있다.
이런 치명적인 문제를 방지하기 위해 JPA에서 지원하는 Lock(낙관적 락 or 비관적 락) 그리고 ConcurrentQueue를 알아본다.
먼저 Lock에도 종류가 있는데
지금 상황이 비관적이므로 비관적 락 부터 알아본다
지금 핫딜을 예시로 쉽게 설명하자면 “내가 지금 구매 할거니까 나 끝날 때까지 다들 기다리고 있어” 라고 보면 된다
JPA에서 제공하고 있기에 레포지토리 메소드에 @Lock 어노테이션을 사용하여 비관적 락을 걸 수 있다
public interface HotdealRepository extends JpaRepository<Hotdeal, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("sampel query")
~~~~void afterCheckHotdealQuantityAndBuy(Long hotdealId);
}
LockModeType의 종류는 아래와 같다
근데 비관적락의 단점은 뭘까 바로 성능적 이슈가 딱 봐도 보일것이다 구매요청이 극단적으로 10,000건이 동시에 몰렸는데 내가 10,000 번째 구매자다? 그럼 바로 9999가 쓰여져 있는 번호표를 들고 눈물을 훔쳐야 할 것…
기본적으로 충돌이 없을 것이라고 가정하고 수행하다 문제가 생겼을 떄 처리를 하는데
@Entity
// 어노테이션 생략
public class Hodeal {
@Id
@GeneratedValue(strategy생략)
private Long id;
@Version
private Long version;
// 아래 생략
}
@Version을 엔티티에 추가해서 사용하면 만약 Entity를 수정할 때마다 JPA가 자체적으로 Versioning을 지원하여 조회 시점의 버전과 수정 시점의 버전이 다르면 예외를 발생시킨다
→ ObjectOptimisticLockingFailureException
예외
허접한 그림판 재출격
@Lock(LockModeType.OPTIMISTIC)
JpaReository에 @Lock 어노테이션을 통해 비관적 락과 마찬가지로 LockModeType이 있으며 아래와 같다
낙관적 락과 비관적 락을 단순히 성능을 보자면 낙관적 락이 압도적으로 좋고 방식도 합리적인데 데이터의 일관성과 무결성을 보장해야 하는 경우나 충돌의 가능성이 매우 높은 상황에서는 비관적 락을 채택해야 할 것 같다.
+Redisson 분산락
@RequiredArgsConstructor
@Service
public class HotdealService {
private final RedissonClient redissonClient;
@Transactional
public void changeDealQuantity throws(Status desiredStatus) {
// 레디스 락 데이터 생성 후, 3초 락
RLock lock = redissonClient.getLock("key 이름");
try {
boolean isLocked = lock.tryLock(2, 3, TimeUnit.SECONDS);
if (!isLocked) {
// 락 획득에 실패했으므로 예외 처리
throw new Error( ... );
}
// 로직
} catch (InterruptedException e) {
// 쓰레드가 인터럽트 될 경우의 예외 처리
} finally {
// 락 해제
lock.unlock();
}
}
}
타임아웃을 설정하여 데드락 방지
tryLock의 첫번째 파라미터로 락 획득을 대기할 시간을, 두번째 파라미터로 락이 만료되는 시간을 설정한다
2초 동안 락을 획득하지 못하면 false를 반환하여 락이 실패됐음을 알려주고, 락을 획득하고 나서 3초가 지나면 자동으로 레디스에 저장된 key:value가 삭제되기 때문에 혹여나 락이 해제되지 않더라도 다른 스레드에서 락을 획득할 수 있는것..