해결된 질문
작성
·
741
·
수정됨
0
@Test
public void 상품_주문() {
//given
Member member = new Member();
member.setName("회원1");
member.setAddress(new Address("서울", "강가", "123-123"));
em.persist(member);
Book book = new Book();
book.setName("시골 JPA");
book.setPrice(10000);
book.setStockQuantity(10);
book.setAuthor("kim");
em.persist(book);
//when
int orderCount = 2; // 두권 주문
Long orderId = this.orderService.order(member.getId(), book.getId(), orderCount);
//then
Order getOrder = this.orderRepository.findOne(orderId);
Assertions.assertEquals(OrderStatus.ORDER, getOrder.getStatus());
}
강의 내용 중 테스트 코드
//==생성 메서드==//
public static OrderItem createOrderItem(Item item, int orderPrice, int count) {
OrderItem orderItem = new OrderItem();
orderItem.setItem(item);
orderItem.setOrderPrice(orderPrice);
orderItem.setCount(count);
item.removeStock(count); // 해당 상품의 재고 수량 차감
return orderItem;
}
강의 내용 중 OrderItem 엔티티 내 생성 메서드 createOrderItem()
강의에서는 위와 같이 OrderItem 엔티티의 생성 메서드라는 것을 통해 주문상품을 만들고 상품의 재고를 감소시키는데요. jpa가 상품 수량(stock_quantity) 감소시킬 때 사용한 UPDATE 쿼리를 보니까 UPDATE item SET stock_quantity = 8; 과 같이 되어있더라고요. 이러면 여러 클라이언트가 동시에 해당 상품 주문할 때 덮어쓰는 문제가 발생하니까 따로 해결 방법을 찾아봤습니다.
JPA에 낙관적 락이라는 게 있길래 적용해봤더니 다른 트랜잭션이 중간에 상품 수량을 변경하고 커밋하면 해당 트랜잭션에서 변환된 스프링 예외(ObjectOptimisticLockingFailureException)가 올라오며 덮어쓰는 문제는 막을 수 있었습니다(테이블 데이터 생성 후 ddl-auto: none 모드로 실행, h2 콘솔과 함께 테스트). 예외를 잡고 새 스냅샷으로 다시 호출할 수 있겠지만, db에 stock_quantity 데이터만 정확히 맞추면 되는 게 목적이어서 createOrderItem 로직을 별도의 리포지토리로 대체해보았습니다.
@Slf4j
@Repository
@RequiredArgsConstructor
public class OrderItemRepository {
private final EntityManager em;
/**
* 주문 상품 생성
* @param item
* @param orderPrice
* @param count
* @return
*/
public OrderItem createOrderItem(Item item, int orderPrice, int count) {
OrderItem orderItem = new OrderItem();
orderItem.setItem(item);
orderItem.setOrderPrice(orderPrice);
orderItem.setCount(count);
int restStock = item.getStockQuantity() - count;
if (restStock < 0) {
log.info("need more stock for {}.{}", item.getId(), item.getName());
throw new NotEnoughStockException("need more stock");
}
// 해당 상품의 재고 수량 차감
em.createQuery("UPDATE Item i " +
"SET stock_quantity = stock_quantity - :count " +
"WHERE item_id = :item_id")
.setParameter("count", count)
.setParameter("item_id", item.getId())
.executeUpdate(); //
em.refresh(item);
return orderItem;
}
em.createQuery()로 item.removeStock(count); 부분을 바꿨습니다. SET stock_quantity = stock_quantity - :count
/** OrderService 내 order 메서드 */
@Transactional(readOnly = false)
public Long order(Long memberId, Long itemId, int count) {
//엔티티 조회
Member member = this.memberRepository.findOne(memberId);
Item item = this.itemRepository.findOne(itemId);
//배송 정보 생성
Delivery delivery = new Delivery();
delivery.setAddress(member.getAddress());
//주문 상품 생성
OrderItem orderItem = this.orderItemRepository.createOrderItem(item, item.getPrice(), count);
//주문 생성
Order order = Order.createOrder(member, delivery, orderItem);
//주문 저장
this.orderRepository.save(order);
return order.getId();
}
기존에 호출하던 OrderItem.createOrderItem(item, item.getPrice(), count); 대신에 orderItemRepository.createOrderItem()를 호출하도록 OrderService 코드를 변경
강의에서 작성한 상품_주문() 테스트를 단일 실행했을 때도 em.createQuery.executeUpdate()할 때 보니까, flush인가 그것도 호출안했는데 jpa가 그전에 persist한 book이랑 member까지는 실제로 인서트하고 업데이트 쿼리를 실행하는 것을 확인했습니다.
[질문]
1. 강의에선 OrderItem 엔티티 자체에서 createOrderItem()을 처리해주기 때문에 OrderItem의 기본 생성자도 protected로 지정해서 막았는데, 글에서 적용한 방식으로 해결하려면 public으로 바꿔야 했습니다. 지금처럼 OrderItemRepository를 따로 만들어서 처리하는 게 구조상 문제가 없는 건지 리포지토리의 역할이 맞는지 모르겠고, 문제가 생길 수 있는지 궁금합니다.
(예측되는 문제점: 주문/주문 취소/상품 수정 등 stock_quantity와 얽혀있는 로직마다 쿼리를 작성해야 됨)
2. JPA의 기본적인 변경 감지 방식을 유지하면서 낙관적 락 없이도 간단하게 해결할 수 있는 방법이 있는지 알고 싶습니다.
답변 2
1
안녕하세요. CS님
문제를 잘 해결하셨네요 :)
이 문제를 해결하려면 다음 2단계로 되어 있는 것을 1단계로 줄이면 됩니다.
값을 읽는다.
값을 수정한다.
작성하신 업데이트 쿼리는 값을 읽는 것과 수정하는 것을 원자적으로 처리하기 때문에 이 경우 동시성 문제를 해결할 수 있습니다.
실제 실무에서도 단순히 update 쿼리로 동시성 문제가 해결되는 경우에는 update 문을 사용해서 동시성을 해결하는 경우가 종종 있습니다.
질문하신 것 처럼 리포지토리에서 update 쿼리가 발생하기 때문에 별도의 리포지토리에서 해당 로직을 호출하도록 작성하는 것이 맞다 생각합니다. 물론 걱정하신 것 처럼 상품의 수량이 변경될 때 마다 해당 리포지토리를 항상 잘 호출하도록 해주어야 합니다. 물론 이 부분은 트레이드 오프입니다. 도메인 로직에 있으면 로직이 더 깔끔해지고 관리 측면에서도 좋겠지만 동시성 문제를 update 쿼리로 해결해야 하기 때문에 해당 부분이 리포지토리에 있어야 합니다.
JPA의 기본적인 변경 방식을 유지하면서 이 문제를 해결할 수 는 없습니다. 동시성 문제와 관련해서 다음 내용들을 읽어보시면 도움이 되실거에요.
감사합니다.
1
안녕하세요, 인프런 AI 인턴이에요.
주문 서비스에서 상품 수량(stock_quantity)을 감소시키는 작업은 동시에 여러 사용자가 요청할 경우 문제가 발생할 수 있습니다. 여러 스레드에서 동시에 상품 수량을 확인하고 감소시키는 경우 잘못된 결과가 나올 수 있습니다.
이런 경우 동시성 문제를 해결하기 위해 다음과 같은 방법들을 고려할 수 있습니다.
하지만, 각 방법마다 장단점이 있으며, 상황에 따라 적절한 방법을 선택해야 합니다. 동시성 문제를 해결하려면 데이터베이스의 트랜잭션 격리 수준 설정이나 잠금 사용 등 전문적인 지식이 필요할 수 있으므로, 신중히 고려해야 해요.
상세한 답변 감사드립니다 영한님.
인프콘 영상에서 기술과 비즈니스의 조화, 추상화와 구체화 넘나들기 등 좋은 말씀 잘들었습니다^^