인프런 커뮤니티 질문&답변

하윤님의 프로필 이미지
하윤

작성한 질문수

재고시스템으로 알아보는 동시성이슈 해결방법

Redis 라이브러리 알아보기

강의보고 토이프로젝트로 재고감소 낙관적락 기법 적용 질문

작성

·

528

0

안녕하세요 강사님 강의를 보고 토이 프로젝트에서 주문 시 재고 감소 및 메뉴 주문량 증가 로직에서 낙관적락 기법을 적용해 보았는데요, 강의에서 해주신 내용 그대로 파사드 패턴 까지 적용을 해보면서 시도했는데 무한 루프가 돌았습니다.

hikariCP pool을 40으로 설정해주니 그제서야 해결이 되었는데요, 강의에선 네임드락에서 커넥션풀을 지정해주었는데요 저는 낙관적락인데도 해당 설정을 해서 해결된 이유가 있을까요?

@Service
@RequiredArgsConstructor
public class OrderService {
    @Transactional
    public Order orderWithOptimisticLock(Long memberId, LocalDateTime now) {

        Cart cart = cartRepository.findByMember(memberId);

        List<CartItem> cartItems = cart.getCartItems();

        cartItems.stream()
                .map(CartItem::getMenu)
                .forEach(menu -> {
                    decreaseStockWithOptimisticLock(menu.getId(), 1);
                    increaseMenuOrderCountWithOptimisticLock(menu.getId(), 1);
                });

        Money money = calculator.calculateMenus(cart.getMember(), cart.convertToMenus());

        Order order = Order.createOrder(cart, money, now);

        return orderRepository.save(order);
    }

    public void decreaseStockWithOptimisticLock(Long menuId, int quantity) {
        Menu menu = menuRepository.findByIdForOptimisticLock(menuId);
        menu.decrease(quantity);
    }

    public void increaseMenuOrderCountWithOptimisticLock(Long menuId, int quantity) {
        Menu menu = menuRepository.findByIdForOptimisticLock(menuId);
        menu.increaseOrderCount(quantity);
    }
}
```
@Component
@RequiredArgsConstructor
public class OptimisticLockStockFacade {

    private final OrderService orderService;

    public Order order(Long memberId, LocalDateTime localDateTime) throws InterruptedException {
        while (true) {
            try {
                return orderService.orderWithOptimisticLock(memberId, localDateTime);
            } catch (Exception e) {
                Thread.sleep(50);
            }
        }
    }
}
```
public interface JpaMenuRepository extends JpaRepository<Menu, Long> {

    @NotNull
    @Lock(LockModeType.OPTIMISTIC)
    @Query("select m from Menu m where m.id = :id")
    Optional<Menu> findByIdForOptimisticLock(@NotNull @Param("id") Long id);
}

답변 2

1

최상용님의 프로필 이미지
최상용
지식공유자

하윤님 안녕하세요.
하윤님께서 문제가 되는 상황이 정확하게 어떠한 상황인지 모르기때문에 정확한 답변은 어렵습니다.
다만, 커넥션풀을 늘려주었을 때 해결이 되는것으로 봐서는 많은 요청이 동시에 들어오고 계속해서 업데이트를 시도하려고 하고 이로인한 충돌이 잦게 일어나면서 많은 스레드가 계속해서 커넥션을 요청하기때문에 커넥션이 부족한 상황 인것 같습니다.
그렇기때문에 커넥션풀을 늘려주었을 때 정상적으로 동작하는 것이라고 유추가 됩니다.

-1

안녕하세요, 인프런 AI 인턴입니다. 하윤님의 질문에 답변 드립니다.

낙관적 락(Optimistic Locking)을 사용하는 경우 데이터베이스에 락을 걸지 않고 트랜잭션을 수행한 뒤, 변경을 커밋할 때 버전 정보를 확인하여 충돌 여부를 판단합니다. 충돌이 감지되는 경우 예외가 발생하고, 이를 애플리케이션 레벨에서 재시도 로직을 통해 다시 처리할 수 있습니다.

하윤님의 구현 코드에서는 OptimisticLockStockFacade에 무한 루프와 재시도 로직이 잘 구현되어 있습니다. 이와 같은 구조에서 무한 루프 상황이 발생한다면 낙관적 락을 재시도하는 과정에서 데이터베이스 커넥션이 다수 필요할 수 있습니다. 트랜잭션이 실패할 때마다 커넥션을 새로이 얻어와야 하며, 동시에 많은 수의 재시도가 발생한다면 커넥션 풀이 부족할 수 있습니다.

HikariCP에서 커넥션 풀 크기를 40으로 설정했을 때 문제가 해결된 것은 재시도 로직을 수행하는 동안에 필요한 커넥션을 충분히 확보할 수 있었기 때문일 가능성이 큽니다. 그러나 이 방법은 근본적인 해결책이라기보다는 일시적인 해결책에 가깝습니다. 커넥션 풀 크기를 늘리는 것은 동시성 문제를 회피하기 위한 조치일 뿐, 여전히 많은 수의 트랜잭션이 동시에 발생한다면 다시 동일한 문제에 직면할 가능성이 큽니다.

보다 근본적인 해결을 위해서는 다음과 같은 방안을 고려해볼 수 있습니다.

  1. 재시도 간의 지연 시간을 적절히 조정하여 시스템 부하를 완화합니다.
  2. 낙관적 락 대신 비관적 락(Pessimistic Locking)을 적용해보는 것도 한 방법입니다.
  3. 트랜잭션 격리 수준이나 락 타임아웃 등 다양한 데이터베이스 설정을 검토합니다.
  4. 주문 시스템의 전체적인 트랜잭션 처리 방법이나 비즈니스 로직을 최적화하여 커넥션의 사용을 줄입니다.

또한, Spring Data JDBC에서는 @Version 어노테이션을 사용하여 낙관적 락을 구현할 수 있다는 점을 참고하세요. 해당 링크를 참조하시면 도움이 되실 겁니다.

Spring Data JDBC - Optimistic Locking

감사합니다.

하윤님의 프로필 이미지
하윤

작성한 질문수

질문하기