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/**
@@ -97,23 +95,39 @@ public OrderInfo createOrder(String userId, List<OrderItemCommand> commands) {
9795 // - 트랜잭션 내부에 외부 I/O 없음, lock holding time 매우 짧음
9896 User user = loadUserForUpdate (userId );
9997
100- Set <Long > productIds = new HashSet <>();
101- List <Product > products = new ArrayList <>();
102- List <OrderItem > orderItems = new ArrayList <>();
98+ // ✅ Deadlock 방지: 상품 ID를 정렬하여 일관된 락 획득 순서 보장
99+ // 여러 상품을 주문할 때, 항상 동일한 순서로 락을 획득하여 deadlock 방지
100+ List <Long > sortedProductIds = commands .stream ()
101+ .map (OrderItemCommand ::productId )
102+ .distinct ()
103+ .sorted ()
104+ .toList ();
103105
104- for (OrderItemCommand command : commands ) {
105- if (!productIds .add (command .productId ())) {
106- throw new CoreException (ErrorType .BAD_REQUEST ,
107- String .format ("상품이 중복되었습니다. (상품 ID: %d)" , command .productId ()));
108- }
106+ // 중복 상품 검증
107+ if (sortedProductIds .size () != commands .size ()) {
108+ throw new CoreException (ErrorType .BAD_REQUEST , "상품이 중복되었습니다." );
109+ }
110+
111+ // 정렬된 순서대로 상품 락 획득 (Deadlock 방지)
112+ Map <Long , Product > productMap = new java .util .HashMap <>();
109113
114+ for (Long productId : sortedProductIds ) {
110115 // 비관적 락을 사용하여 상품 조회 (재고 차감 시 동시성 제어)
111116 // - id는 PK 인덱스가 있어 Lock 범위 최소화 (Record Lock만 적용)
112117 // - Lost Update 방지: 동시 주문 시 재고 음수 방지 및 정확한 차감 보장 (재고 oversell 방지)
113118 // - 트랜잭션 내부에 외부 I/O 없음, lock holding time 매우 짧음
114- Product product = productRepository .findByIdForUpdate (command .productId ())
119+ // - ✅ 정렬된 순서로 락 획득하여 deadlock 방지
120+ Product product = productRepository .findByIdForUpdate (productId )
115121 .orElseThrow (() -> new CoreException (ErrorType .NOT_FOUND ,
116- String .format ("상품을 찾을 수 없습니다. (상품 ID: %d)" , command .productId ())));
122+ String .format ("상품을 찾을 수 없습니다. (상품 ID: %d)" , productId )));
123+ productMap .put (productId , product );
124+ }
125+
126+ // OrderItem 생성
127+ List <Product > products = new ArrayList <>();
128+ List <OrderItem > orderItems = new ArrayList <>();
129+ for (OrderItemCommand command : commands ) {
130+ Product product = productMap .get (command .productId ());
117131 products .add (product );
118132
119133 orderItems .add (OrderItem .of (
@@ -147,6 +161,13 @@ public OrderInfo createOrder(String userId, List<OrderItemCommand> commands) {
147161
148162 /**
149163 * 주문을 취소하고 포인트를 환불하며 재고를 원복한다.
164+ * <p>
165+ * <b>동시성 제어:</b>
166+ * <ul>
167+ * <li><b>비관적 락 사용:</b> 재고 원복 시 동시성 제어를 위해 findByIdForUpdate 사용</li>
168+ * <li><b>Deadlock 방지:</b> 상품 ID를 정렬하여 일관된 락 획득 순서 보장</li>
169+ * </ul>
170+ * </p>
150171 *
151172 * @param order 주문 엔티티
152173 * @param user 사용자 엔티티
@@ -157,18 +178,41 @@ public void cancelOrder(Order order, User user) {
157178 throw new CoreException (ErrorType .BAD_REQUEST , "취소할 주문과 사용자 정보는 필수입니다." );
158179 }
159180
160- List <Product > products = order .getItems ().stream ()
161- .map (item -> productRepository .findById (item .getProductId ())
181+ // ✅ Deadlock 방지: User 락을 먼저 획득하여 createOrder와 동일한 락 획득 순서 보장
182+ // createOrder: User 락 → Product 락 (정렬됨)
183+ // cancelOrder: User 락 → Product 락 (정렬됨) - 동일한 순서로 락 획득
184+ User lockedUser = userRepository .findByUserIdForUpdate (user .getUserId ());
185+ if (lockedUser == null ) {
186+ throw new CoreException (ErrorType .NOT_FOUND , "사용자를 찾을 수 없습니다." );
187+ }
188+
189+ // ✅ Deadlock 방지: 상품 ID를 정렬하여 일관된 락 획득 순서 보장
190+ List <Long > sortedProductIds = order .getItems ().stream ()
191+ .map (OrderItem ::getProductId )
192+ .distinct ()
193+ .sorted ()
194+ .toList ();
195+
196+ // 정렬된 순서대로 상품 락 획득 (Deadlock 방지)
197+ Map <Long , Product > productMap = new java .util .HashMap <>();
198+ for (Long productId : sortedProductIds ) {
199+ Product product = productRepository .findByIdForUpdate (productId )
162200 .orElseThrow (() -> new CoreException (ErrorType .NOT_FOUND ,
163- String .format ("상품을 찾을 수 없습니다. (상품 ID: %d)" , item .getProductId ()))))
201+ String .format ("상품을 찾을 수 없습니다. (상품 ID: %d)" , productId )));
202+ productMap .put (productId , product );
203+ }
204+
205+ // OrderItem 순서대로 Product 리스트 생성
206+ List <Product > products = order .getItems ().stream ()
207+ .map (item -> productMap .get (item .getProductId ()))
164208 .toList ();
165209
166210 order .cancel ();
167211 increaseStocksForOrderItems (order .getItems (), products );
168- user .receivePoint (Point .of ((long ) order .getTotalAmount ()));
212+ lockedUser .receivePoint (Point .of ((long ) order .getTotalAmount ()));
169213
170214 products .forEach (productRepository ::save );
171- userRepository .save (user );
215+ userRepository .save (lockedUser );
172216 orderRepository .save (order );
173217 }
174218
0 commit comments