[성능 개선] 상품 주문, 결제 시 비관적 Lock으로 동시성 제어

최혜원·2024년 9월 22일
1

낙관적 락? 비관적 락?

낙관적 락은 읽기 작업이 많을 경우 성능상 이점이 있다. 데이터를 읽을 때 DB 레벨에서 락을 걸지 않기 때문이다. 이로 인해 데드락 또한 발생할 여지가 없다. 하지만 가정 자체가 트랜잭션 간 충돌이 발생하지 않는 것이기 때문에, 쓰기 작업에 대한 동시성 처리가 빈번한 경우에는 적합하지 않다.

반면에 비관적 락은 쓰기 작업이 빈번할 경우에 적합한 락 전략이다. 데이터를 읽기 위해 DB 레벨에서 락을 걸기 때문에 동시에 들어오는 요청에 대해서 안전하게 처리할 수 있다. 하지만 단순한 읽기 작업에도 레코드에 대한 락을 걸어야하기 때문에 읽기 작업이 빈번한 상황에는 적합하지 않을 수 있다.

=> 비관적 락을 사용했다. 낙관적 락은 트래내잭션의 충돌이 발생하지 않는다고 가정하에 진행하기 때문에 다소 위험 부담이 있을 수 있다고 생각했고 실제로 충돌이 나면 개발자가 일일이 롤백을 해주어야 하는 문제가 있다. 그래서 비관적 락을 사용하여 충돌이 발생하지 않더라도 일단 락을 걸고보는 방식을 택했다.

curl을 이용한 동시성 테스트 - 10명이 주문을 동시에 하는 상황

(현재는 결제까지 완료해야 재고가 줄어들지만, 결제기능 구현 전 주문하기 버튼 누르면 재고 감소하는 로직)

1. 상품 재고 10개

락 걸기 전 데드락 발생

데드락(교착상태)란 두개 이상의 작업이 서로 상대방의 작업이 끝나기 만을 기다리고 있기 때문에 결과적으로 아무것도 완료되지 못하는 상태를 가리킨다. 한정된 자원을 여러 곳에서 사용하려고 할 때 발생할 수 있다.

2. 비관적 락 구현

 	@Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT i FROM Item i WHERE i.no = :no")
    Optional<Item> findByIdWithLock(@Param("no") Long no);
  • 동시성 제어를 위한 데이터베이스 수준의 락
  • 다른 트랜잭션의 접근을 차단
  • 'SELECT FOR UPDATE' 구문으로 변환됨
    @Transactional
    public Long order(OrderItemRequestDto orderItemDto, Member member) {
        // 선택한 상품 주문
        Item item = itemRepository.findByIdWithLock(orderItemDto.getItemNo()).orElseThrow(
            () -> new BusinessException(ErrorCode.NOT_FOUND_ITEM)
        );
        // 이전 주문(결제하지 않은, 주문 상태 ORDER) 이 있는지 확인
        Order existOrder = getStatusOrder(member);
        if (existOrder != null) { // 이전 주문이 있으면 삭제
            orderRepository.delete(existOrder);
        }
        List<OrderItem> orderItemList = new ArrayList<>(); // 주문 상품 담는 리스트
        orderItemList.add(orderItemDto.toEntity(item)); // (상품 담아) 주문 상품 생성
        Order order = Order.toEntity(member, orderItemList, false); // (주문 상품 담아) 주문 생성
        orderRepository.save(order); // 주문 저장
        return order.getNo();
    }
- 미결제 주문 존재 시 삭제
- 새로운 주문 생성 및 저장

사용 목적

  • 동시 업데이트 방지
  • 데이터 일관성 보장
  • race condition 예방

동작 방식

  • 아이템 조회 시 해당 row에 락 설정
  • 트랜잭션 완료될 때까지 다른 트랜잭션의 접근 차단
  • 트랜잭션 종료 시 락 해제

3. 10명 복숭아 동시 주문

curl --location 'http://localhost:8080/api/orders' \
--header 'Content-Type: application/json' \
--header 'Cookie: Authorization=Bearer%20eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiIke0pXVF9JU1NVRVJ9IiwiaWF0IjoxNzI0MTU3MjMyLCJleHAiOjE3MjQxNTkwMzIsInN1YiI6ImVzYzAyMTgiLCJubyI6MSwicm9sZSI6IlJPTEVfQURNSU4ifQ._Z8cAc5RCVkRwLb-WfD_K0pb0sVjQMffFB490iz-TIE' \
--data '{
    "itemNo" : 1,
    "count" : 1
}'&curl --location 'http://localhost:8080/api/orders' \
--header 'Content-Type: application/json' \
--header 'Cookie: Authorization=Bearer%20eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiIke0pXVF9JU1NVRVJ9IiwiaWF0IjoxNzI0MTU3MzE2LCJleHAiOjE3MjQxNTkxMTYsInN1YiI6ImVzYzAyMTkiLCJubyI6Miwicm9sZSI6IlJPTEVfTUVNQkVSIn0.zdpa5aQyWEC43J_cr0j2AnIPpyhOxiijAxxQX49wKzQ' \
--data '{
    "itemNo" : 1,
    "count" : 1
}'&curl --location 'http://localhost:8080/api/orders' \
--header 'Content-Type: application/json' \
--header 'Cookie: Authorization=Bearer%20eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiIke0pXVF9JU1NVRVJ9IiwiaWF0IjoxNzI0MTU3MzYyLCJleHAiOjE3MjQxNTkxNjIsInN1YiI6ImVzYzAyMjAiLCJubyI6Mywicm9sZSI6IlJPTEVfTUVNQkVSIn0.U-Xx5ABfIG74Vrym_PsjNR_ru2wA7lSiFYdsmPp-iMs' \
--data '{
    "itemNo" : 1,
    "count" : 1
}'&curl --location 'http://localhost:8080/api/orders' \
--header 'Content-Type: application/json' \
--header 'Cookie: Authorization=Bearer%20eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiIke0pXVF9JU1NVRVJ9IiwiaWF0IjoxNzI0MTU3Mzg0LCJleHAiOjE3MjQxNTkxODQsInN1YiI6ImVzYzAyMjEiLCJubyI6NCwicm9sZSI6IlJPTEVfTUVNQkVSIn0.qbquriyQMaoR1rl6aOUKBu0V1yotThgngwJy-GqlIKk' \
--data '{
    "itemNo" : 1,
    "count" : 1
}'&curl --location 'http://localhost:8080/api/orders' \
--header 'Content-Type: application/json' \
--header 'Cookie: Authorization=Bearer%20eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiIke0pXVF9JU1NVRVJ9IiwiaWF0IjoxNzI0MTU3NDAxLCJleHAiOjE3MjQxNTkyMDEsInN1YiI6ImVzYzAyMjIiLCJubyI6NSwicm9sZSI6IlJPTEVfTUVNQkVSIn0.dgaPi_dWpT4bqdaGSdr5PwgGxWLTaNW8QzaSzJrNPmE' \
--data '{
    "itemNo" : 1,
    "count" : 1
}'&curl --location 'http://localhost:8080/api/orders' \
--header 'Content-Type: application/json' \
--header 'Cookie: Authorization=Bearer%20eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiIke0pXVF9JU1NVRVJ9IiwiaWF0IjoxNzI0MTU3NDE3LCJleHAiOjE3MjQxNTkyMTcsInN1YiI6ImVzYzAyMjMiLCJubyI6Niwicm9sZSI6IlJPTEVfTUVNQkVSIn0.f08EfENXd4-Dw731I9ENCngnopGOSylUdPSAdtydtmw' \
--data '{
    "itemNo" : 1,
    "count" : 1
}'&curl --location 'http://localhost:8080/api/orders' \
--header 'Content-Type: application/json' \
--header 'Cookie: Authorization=Bearer%20eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiIke0pXVF9JU1NVRVJ9IiwiaWF0IjoxNzI0MTU3NDMzLCJleHAiOjE3MjQxNTkyMzMsInN1YiI6ImVzYzAyMjQiLCJubyI6Nywicm9sZSI6IlJPTEVfTUVNQkVSIn0.tismQlWqGSwTryA_HYfbmgFzAaHiPoZt9nzSt0M0pC8' \
--data '{
    "itemNo" : 1,
    "count" : 1
}'&curl --location 'http://localhost:8080/api/orders' \
--header 'Content-Type: application/json' \
--header 'Cookie: Authorization=Bearer%20eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiIke0pXVF9JU1NVRVJ9IiwiaWF0IjoxNzI0MTU3NDQ4LCJleHAiOjE3MjQxNTkyNDgsInN1YiI6ImVzYzAyMjUiLCJubyI6OCwicm9sZSI6IlJPTEVfTUVNQkVSIn0._6K_OC0fmsqmUZE3IphfrGpCaVx2ZS_kkWVa29a-1so' \
--data '{
    "itemNo" : 1,
    "count" : 1
}'&curl --location 'http://localhost:8080/api/orders' \
--header 'Content-Type: application/json' \
--header 'Cookie: Authorization=Bearer%20eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiIke0pXVF9JU1NVRVJ9IiwiaWF0IjoxNzI0MTU3NDcxLCJleHAiOjE3MjQxNTkyNzEsInN1YiI6ImVzYzAyMjYiLCJubyI6OSwicm9sZSI6IlJPTEVfTUVNQkVSIn0.4QVbVjSKw_qWa38rcOgcWm4ivZffh2sHQPoZQIMFk3I' \
--data '{
    "itemNo" : 1,
    "count" : 1
}'&curl --location 'http://localhost:8080/api/orders' \
--header 'Content-Type: application/json' \
--header 'Cookie: Authorization=Bearer%20eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiIke0pXVF9JU1NVRVJ9IiwiaWF0IjoxNzI0MTU3NDg1LCJleHAiOjE3MjQxNTkyODUsInN1YiI6ImVzYzAyMjciLCJubyI6MTAsInJvbGUiOiJST0xFX01FTUJFUiJ9.rqdMfPF8JrwhtB53HMnSUHzGBbfN87KZLyuGZYcbrT4' \
--data '{
    "itemNo" : 1,
    "count" : 1
}'

4. 상품 entity Lock을 걸고 데이터를 가져온다.

5. 주문 상품 수만큼 재고 감소하여 재고 0

6. 주문 상품과 주문 데이터가 회원1번부터 10번까지 잘 생성되었다.

주문상품 테이블
item_no : 1(복숭아)

주문 테이블

profile
어제보다 나은 오늘

0개의 댓글