[BooTakHae] 위시리스트 내 전체 상품 주문 시 Lock 적용 방식

Kim Hyen Su·2024년 5월 16일

BooTakHae

목록 보기
3/22
post-thumbnail

다수 상품 재고 처리 시 Lock 적용 방법


💡 읽기 전 꼭 확인해주세요!
해당 포스팅은 시리즈로 이어지며, 목록은 다음과 같습니다! :)

개요

단일 상품 단위로 생각해봤을 때는 몰랐지만, 위시리스트 내 전체 상품을 한번에 주문할 경우, Lock을 어떻게 걸어야 할까라는 의문이 들었습니다.

처음에는 어차피 리스트 내 모든 상품이 다른 종류의 상품이니 상관없다라고 생각했었습니다. 따라서, 위시리스트의 첫번째 항목에만 Lock을 걸어서 재고를 처리하도록 구현했었습니다.

기존 로직

@Slf4j
@Component
@RequiredArgsConstructor
public class RedissonInventoryFacade {
    private final RedissonClient redissonClient;

    private final ProductService productService;

    public List<ProductDto> update(RequestStock request){
        log.debug("분산락 실행 - 재고 수량 변경 실행 : {}", request.getStockProcess());

        List<ProductInfo> productInfoList = request.getProductInfoList();

        ProductInfo firstInfo = productInfoList.getFirst();
        RLock lock = redissonClient.getLock(firstInfo.getProductId());

        try{
            boolean available = lock.tryLock(100, 1, TimeUnit.SECONDS);

            if(!available){
                throw new CustomException(ErrorCode.LOCK_NOT_AVAILABLE);
            }

           return productService.updateStock(request);
        }catch(InterruptedException e){
            throw new RuntimeException(e);
        }finally {
            lock.unlock();
        }
    }
}

하지만, 기존의 방식대로 구현하게 될 경우, 첫번째 항목 이외의 상품에는 Lock이 걸리지 않게 되므로, 해당 시점에 다른 사용자가 해당 상품을 주문한 경우 동시성 문제가 발생하게 됩니다.

위의 상황을 테스트하기 위해서 테스트 코드를 작성했습니다.

Test

상황을 다음과 같이 가정해보겠습니다.

  • 회원1이 상품 A,B,C를 위시리스트에 1개씩 담은 상태입니다.
  • 회원2가 상품 B,C,D를 위시리스트에 1개씩 담은 상태입니다.

위의 상태에서 JMeter를 사용하여 ThreadCount 500, ramp-up time 1, loop count 3으로 설정한 뒤 테스트를 진행했습니다.

그 결과 아래의 그림처럼 동시성 문제가 발생하였습니다.

예상 했던대로 실제로 Lock이 걸리지 않았던 상품들은 동시성 제어가 되지 않고 있습니다.

이를 해결하기 위해서 각 상품마다 Lock을 걸도록 로직을 수정했습니다.

수정 후 로직

public List<ProductDto> updateList(RequestStock request){
        log.debug("각 항목 분산락 실행 - 재고 수량 변경 실행 : {}", request.getStockProcess());

        List<ProductInfo> productInfoList = request.getProductInfoList();
        Map<String, RLock> locks = new LinkedHashMap<>(); // 상품 ID와 해당 락을 매핑하기 위한 Map

        try {
            for (ProductInfo info : productInfoList) {
                RLock lock = redissonClient.getLock(info.getProductId());
                boolean available = lock.tryLock(100, 10, TimeUnit.SECONDS); // 락 획득 시도

                if (!available) { // 락 획득 실패 시
                    throw new CustomException(ErrorCode.LOCK_NOT_AVAILABLE);
                }

                locks.put(info.getProductId(), lock); // 성공적으로 락을 획득했으면 Map에 추가
            }

            // 모든 상품에 대한 락 획득 후 재고 업데이트 처리
            return productService.updateStock(request);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt(); // 현재 스레드의 interrupt 상태를 설정
            throw new RuntimeException(e);
        } finally {
            // 모든 락을 해제합니다.
            for (RLock lock : locks.values()) {
                if (lock.isHeldByCurrentThread()) { // 현재 스레드가 락을 보유하고 있는지 확인
                    lock.unlock();
                }
            }
        }
    }

이전과 동일한 조건으로 테스트한 결과 다음의 결과가 출력됐습니다.

위처럼 동시성 문제 없이 정상적으로 처리되는 것을 확인할 수 있었습니다.

새로운 문제

하지만, 위 방식 또한 문제가 발생했습니다.

전체 상품에 대한 Lock을 각각 걸어준 뒤에 한번에 Lock을 반납하기 때문에 트랜잭션 단위로 Lock이 발생하게 됩니다.

위와 같은 경우, 성능상 병목현상으로 인해 속도가 현저히 떨어지게 됩니다.

트레이드 오프

  • Redisson
    • DB 부하 감소
    • 성능 저하의 원인
  • Pessimistic Lock
    • DB 부하 증가 및 해당 트랜잭션 처리 비용 증가
    • Redisson 방식에 비해서 성능이 빠르고, 동시성 제어를 100% 보장

결론

필자의 프로젝트는 사용자에게 빠르고 정확한 주문처리를 통해 사용자 만족도를 높이는 것을 주요 목적으로 가지기 때문에, 현재 상황에서 Pessimistic Lock을 적용하는 것이 옳다고 판단되어 구현하였습니다.

profile
백엔드 서버 엔지니어

0개의 댓글