1. 주문 서비스에 락을 적용 해보고 락이 잘 적용되는 것 까지 확인을 했는데도 동시성 문제가 해결되지 않았던 이슈

<aside> ❗ 락을 적용시키고 재고감소는 락 내부에서 했지만, 실제 재고의 update쿼리가 락 외부에서 날아가던 문제가 발생하였습니다.

</aside>

문제가 발생했던 코드

/* OrderService.java */
@Transactional
public void orderBasketProducts(String orderRequest) throws JsonProcessingException {

	.
	.
	.
    for (OrderProduct orderProduct : orderProductList) {

        Long productId = orderProduct.getProduct().getId();
        RLock lock = redissonClient.getLock("product" + productId); //상품ID별로 락 획득
        try {
            boolean available = lock.tryLock(10, 5, TimeUnit.SECONDS); // 락 획득 시도

            if (!available) {
                log.error("주문 시도 중 lock 획득 실패");
                continue;
            }

            productService.decrease(productId, orderProduct.getCount()); // 재고를 감소시키는 부분

        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } catch (IllegalArgumentException e) {
            log.error("재고 부족으로 구매 실패");
        } finally {
            lock.unlock(); // 락 해제
        }
    }
}

💡 문제 개선

문제는 락과 트랜잭션의 범위와 @Transactional 의 동작원리에 있었습니다.

@Transactional 어노테이션이 붙어있으면 기본적으로 해당 서비스(OrderService)에서 로직을 호출하지 않고, ProductService를 필드 변수로 주입받은 다른 객체에서 OrderService의 메소드를 호출해 작업을 수행하게 됩니다.

/* ProductService를 필드변수로 주입 받은 다른 객체*/
.
.
.

// 트랜잭션 생성

productService.decrease();
													//<- 이 사이에 다른 쓰레드가 락을 점유하고 수정되기 이전 값을 변경시킴

// 트랜잭션 커밋

.
.
.

이 과정에서 해당 가짜 객체는 대략적으로 위와같은 방식으로 트랜잭션을 열고, 서비스로직을 수행하고, 해당 변경사항을 반영하는데요.

변경사항이 반영되기 이전에 락이 풀리고 다른 쓰레드가 락을 점유하게 되어 데이터의 동시성 문제가 발생하였습니다.

트랜잭션의 범위를 락보다 작게 잡아 개선

/* OrderService.java */
public void orderBasketProducts(String orderRequest) throws JsonProcessingException {

	.
	.
	.
    for (OrderProduct orderProduct : orderProductList) {

        Long productId = orderProduct.getProduct().getId();
        RLock lock = redissonClient.getLock("product" + productId); //상품ID별로 락 획득
        try {
            boolean available = lock.tryLock(10, 5, TimeUnit.SECONDS);

            if (!available) {
                log.error("주문 시도 중 lock 획득 실패")
                continue;
            }

            productService.decrease(productId, orderProduct.getCount());
						// 메소드가 끝나고 트랜잭션이 commit된 후 finally에서 unlock();

        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } catch (IllegalArgumentException e) {
            log.error("재고 부족으로 구매 실패");
        } finally {
            lock.unlock();
        }
    }
}

OrderService의 주문 메소드 자체에 걸었던 @Transactional 어노테이션을 제거하고, 아래와 같이 락 내부 재고를 감소시키는 ProductService.decrease()로 트랜잭션 범위를 좁혀 락 내부에서 update쿼리가 날아갈 수 있도록 개선하였습니다.