저번 포스트에서는 내가 구현하고 있는 프로젝트에서 어떠한 동시성 문제가 생길 수 있을까에 대해 고민해보았다. 나는 결제에 대해서는 많은 접근이 일어나지 않을 것이라고 판단하였고 때문에 성능적으로 더 이점을 줄 수 있는 낙관적 락을 채택하였다.(통합테스트 통과 못해서 힘들었...)
또한 동시에 여러 사람이 동일 좌석 예약을 시도할 때는 서비스 특성 상 가장 많이 사람들이 접근할 것이라고 생각하였고, 조회가 빈번할 것이기 때문에 비관적 락을 채택하였다.
수정에 실패했을 때, 해당 비즈니스 로직의 실패로 이어져도 되는 경우여야 한다.
비관적 락은 낙관적 락에 비해 잠금을 위한 범위가 커질 수 있다.
수정과 조회가 빈번하게 이루어지는 경우 쿼리에서는 락 경합이 빈번하게 발생할 수 있다.
또한 락이 잠그는 범위가 큰 경우는, 개발자가 의도하지 않은 다른 테이블에 대한 조회에서도 비관적 락에 대한 대기 문제가 발생할 수 있다.
하지만 비관적 락은 '빠르게 처리 가능'하며 '반드시 순서대로 성공해야 하는 작업'에 대해서 매우 효과적인 해결책이 될 수 있다.
Redis 등을 활용한 외부 Resource 를 통해 불필요한 DB Connection 까지 차단 가능하다.
하지만 관리주체가 DB + Redis 와 같이 늘어남에 따라 다양한 문제 파생으로 이어진다.
또한 Lock 의 관리주체가 다운되면 서비스 전체의 Down 으로 이어질 수 있는 문제가 있다.
하지만 Redis 의 높은 원자성을 활용해 프로세스 처리단위에 대한 동일한 Lock 을
여러 인스턴스에 대해 적용할 수 있으므로 매우 효과적일 수 있고, DB 의 Conection 이나
오래 걸리는 I/O 에 대한 접근 자체를 차단할 수 있으므로 DB 에 가해지는 직접적인 부하를
원천 차단할 수 있으므로 효과적이다.
@Transactional
public void charge(Long userId, BigDecimal point) {
RLock lock = redissonClient.getLock("userChargeLock")
try {
if(lock.tryLock() == true) {
User user = userRepository.findById(userId)
user.charge(point)
} else {
throw new LockAccruedFailedException();
}
} finally {
lock.unlock();
}
}
@Transactional
public void pay(Long userId, BigDecimal point) {
RLock lock = redissonClient.getLock("userPayLock")
try {
if(lock.tryLock() == true) {
User user = userRepository.findById(userId)
user.pay(point)
} else {
throw new LockAccruedFailedException();
}
} finally {
lock.unlock();
}
}
public void charge(Long userId, BigDecimal point) {
RLock lock = redissonClient.getLock("user:" + userId);
try {
if (lock.tryLock()) {
executeInTransaction(() -> {
User user = userRepository.findById(userId);
user.charge(point);
});
} else {
throw new LockAcquireFailedException();
}
} finally {
lock.unlock();
}
}
public void pay(Long userId, BigDecimal point) {
RLock lock = redissonClient.getLock("user:" + userId);
try {
if (lock.tryLock()) {
executeInTransaction(() -> {
User user = userRepository.findById(userId);
user.pay(point);
});
} else {
throw new LockAcquireFailedException();
}
} finally {
lock.unlock();
}
}
@Transactional
private void executeInTransaction(Runnable runnable) {
runnable.run();
}
참고