2121import org .springframework .stereotype .Component ;
2222
2323import java .util .ArrayList ;
24- import java .util .HashSet ;
2524import java .util .List ;
2625import java .util .Map ;
2726import java .util .Objects ;
28- import java .util .Set ;
2927import java .util .stream .Collectors ;
3028
3129/**
@@ -100,23 +98,39 @@ public OrderInfo createOrder(String userId, List<OrderItemCommand> commands) {
10098 // - 트랜잭션 내부에 외부 I/O 없음, lock holding time 매우 짧음
10199 User user = loadUserForUpdate (userId );
102100
103- Set <Long > productIds = new HashSet <>();
104- List <Product > products = new ArrayList <>();
105- List <OrderItem > orderItems = new ArrayList <>();
101+ // ✅ Deadlock 방지: 상품 ID를 정렬하여 일관된 락 획득 순서 보장
102+ // 여러 상품을 주문할 때, 항상 동일한 순서로 락을 획득하여 deadlock 방지
103+ List <Long > sortedProductIds = commands .stream ()
104+ .map (OrderItemCommand ::productId )
105+ .distinct ()
106+ .sorted ()
107+ .toList ();
106108
107- for (OrderItemCommand command : commands ) {
108- if (!productIds .add (command .productId ())) {
109- throw new CoreException (ErrorType .BAD_REQUEST ,
110- String .format ("상품이 중복되었습니다. (상품 ID: %d)" , command .productId ()));
111- }
109+ // 중복 상품 검증
110+ if (sortedProductIds .size () != commands .size ()) {
111+ throw new CoreException (ErrorType .BAD_REQUEST , "상품이 중복되었습니다." );
112+ }
113+
114+ // 정렬된 순서대로 상품 락 획득 (Deadlock 방지)
115+ Map <Long , Product > productMap = new java .util .HashMap <>();
112116
117+ for (Long productId : sortedProductIds ) {
113118 // 비관적 락을 사용하여 상품 조회 (재고 차감 시 동시성 제어)
114119 // - id는 PK 인덱스가 있어 Lock 범위 최소화 (Record Lock만 적용)
115120 // - Lost Update 방지: 동시 주문 시 재고 음수 방지 및 정확한 차감 보장 (재고 oversell 방지)
116121 // - 트랜잭션 내부에 외부 I/O 없음, lock holding time 매우 짧음
117- Product product = productRepository .findByIdForUpdate (command .productId ())
122+ // - ✅ 정렬된 순서로 락 획득하여 deadlock 방지
123+ Product product = productRepository .findByIdForUpdate (productId )
118124 .orElseThrow (() -> new CoreException (ErrorType .NOT_FOUND ,
119- String .format ("상품을 찾을 수 없습니다. (상품 ID: %d)" , command .productId ())));
125+ String .format ("상품을 찾을 수 없습니다. (상품 ID: %d)" , productId )));
126+ productMap .put (productId , product );
127+ }
128+
129+ // OrderItem 생성
130+ List <Product > products = new ArrayList <>();
131+ List <OrderItem > orderItems = new ArrayList <>();
132+ for (OrderItemCommand command : commands ) {
133+ Product product = productMap .get (command .productId ());
120134 products .add (product );
121135
122136 orderItems .add (OrderItem .of (
@@ -150,6 +164,13 @@ public OrderInfo createOrder(String userId, List<OrderItemCommand> commands) {
150164
151165 /**
152166 * 주문을 취소하고 포인트를 환불하며 재고를 원복한다.
167+ * <p>
168+ * <b>동시성 제어:</b>
169+ * <ul>
170+ * <li><b>비관적 락 사용:</b> 재고 원복 시 동시성 제어를 위해 findByIdForUpdate 사용</li>
171+ * <li><b>Deadlock 방지:</b> 상품 ID를 정렬하여 일관된 락 획득 순서 보장</li>
172+ * </ul>
173+ * </p>
153174 *
154175 * @param order 주문 엔티티
155176 * @param user 사용자 엔티티
@@ -160,18 +181,41 @@ public void cancelOrder(Order order, User user) {
160181 throw new CoreException (ErrorType .BAD_REQUEST , "취소할 주문과 사용자 정보는 필수입니다." );
161182 }
162183
163- List <Product > products = order .getItems ().stream ()
164- .map (item -> productRepository .findById (item .getProductId ())
184+ // ✅ Deadlock 방지: User 락을 먼저 획득하여 createOrder와 동일한 락 획득 순서 보장
185+ // createOrder: User 락 → Product 락 (정렬됨)
186+ // cancelOrder: User 락 → Product 락 (정렬됨) - 동일한 순서로 락 획득
187+ User lockedUser = userRepository .findByUserIdForUpdate (user .getUserId ());
188+ if (lockedUser == null ) {
189+ throw new CoreException (ErrorType .NOT_FOUND , "사용자를 찾을 수 없습니다." );
190+ }
191+
192+ // ✅ Deadlock 방지: 상품 ID를 정렬하여 일관된 락 획득 순서 보장
193+ List <Long > sortedProductIds = order .getItems ().stream ()
194+ .map (OrderItem ::getProductId )
195+ .distinct ()
196+ .sorted ()
197+ .toList ();
198+
199+ // 정렬된 순서대로 상품 락 획득 (Deadlock 방지)
200+ Map <Long , Product > productMap = new java .util .HashMap <>();
201+ for (Long productId : sortedProductIds ) {
202+ Product product = productRepository .findByIdForUpdate (productId )
165203 .orElseThrow (() -> new CoreException (ErrorType .NOT_FOUND ,
166- String .format ("상품을 찾을 수 없습니다. (상품 ID: %d)" , item .getProductId ()))))
204+ String .format ("상품을 찾을 수 없습니다. (상품 ID: %d)" , productId )));
205+ productMap .put (productId , product );
206+ }
207+
208+ // OrderItem 순서대로 Product 리스트 생성
209+ List <Product > products = order .getItems ().stream ()
210+ .map (item -> productMap .get (item .getProductId ()))
167211 .toList ();
168212
169213 order .cancel ();
170214 increaseStocksForOrderItems (order .getItems (), products );
171- user .receivePoint (Point .of ((long ) order .getTotalAmount ()));
215+ lockedUser .receivePoint (Point .of ((long ) order .getTotalAmount ()));
172216
173217 products .forEach (productRepository ::save );
174- userRepository .save (user );
218+ userRepository .save (lockedUser );
175219 orderRepository .save (order );
176220 }
177221
0 commit comments