Notice
Recent Posts
Recent Comments
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 | 31 |
Tags
- C programming
- Selection Sorting
- ㅅ
- 알기쉬운 알고리즘
- 메모리구조
- 윤성우 열혈자료구조
- R
- C 언어 코딩 도장
- JSON
- buffer
- list 컬렉션
- 혼자 공부하는 C언어
- datastructure
- 이것이 자바다
- s
- coding test
- Serialization
- 이스케이프 문자
- Graph
- Stack
- 윤성우의 열혈 자료구조
- stream
- insertion sort
- Algorithm
Archives
- Today
- Total
Engineering Note
[SW Engineering] 이커머스 동시성 문제 해결 본문
문제 상황
이커머스 도메인의 특성상 재고 관리가 중요하기 때문에 여러 사용자가 동시에 같은 상품을 주문하는 상황을 가정하고 테스트를 해보았습니다.
OrderService.java의 order 메서드
public Long order(OrderDto orderDto, String email) {
Item item = itemRepository.findById(orderDto.getItemId()).orElseThrow(EntityNotFoundException::new);
Member member = memberRepository.findByEmail(email);
List<OrderItem> orderItemList = new ArrayList<>();
OrderItem orderItem = OrderItem.createOrderItem(item, orderDto.getQuantity());
orderItemList.add(orderItem);
Order order = Order.createOrder(member, orderItemList);
orderRepository.save(order);
return order.getId();
}
동시성 제어 테스트 코드
@Test
@DisplayName("10명이 동시에 같은 상품 주문")
void lostUpdateProblemTest() throws InterruptedException {
// given: 재고가 충분한 상품
Item item = new Item();
item.setItemNm("인기 상품");
item.setPrice(10000);
item.setItemDetail("재고가 충분한 상품");
item.setItemSellStatus(ItemSellStatus.SELL);
item.setStockNumber(100); // 충분한 재고
item = itemRepository.save(item);
// 10명의 회원 생성
int memberCount = 10;
for (int i = 0; i < memberCount; i++) {
Member member = new Member();
member.setEmail("user" + i + "@test.com");
member.setName("사용자" + i);
memberRepository.save(member);
}
final Long itemId = item.getId();
// when: 10명이 동시에 2개씩 주문 (총 20개 주문)
int threadCount = 10;
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
CountDownLatch latch = new CountDownLatch(threadCount);
AtomicInteger successCount = new AtomicInteger(0);
for (int i = 0; i < threadCount; i++) {
final int userIndex = i;
executorService.execute(() -> {
try {
OrderDto orderDto = new OrderDto();
orderDto.setItemId(itemId);
orderDto.setQuantity(2);
orderService.order(orderDto, "user" + userIndex + "@test.com");
successCount.incrementAndGet();
} catch (Exception e) {
System.err.println("주문 실패: " + e.getMessage());
} finally {
latch.countDown();
}
});
}
latch.await();
executorService.shutdown();
// then: Lost Update 문제 확인
Item resultItem = itemRepository.findById(itemId).orElseThrow();
int expectedStock = 100 - (successCount.get() * 2);
int actualStock = resultItem.getStockNumber();
System.out.println("=== Lost Update 문제 재현 결과 ===");
System.out.println("성공한 주문 수: " + successCount.get() + "명");
System.out.println("주문한 총 수량: " + (successCount.get() * 2) + "개");
System.out.println("예상 재고: " + expectedStock + "개");
System.out.println("실제 재고: " + actualStock + "개");
System.out.println("재고 차이: " + (actualStock - expectedStock) + "개");
// Lost Update가 발생하면 실제 재고가 예상보다 많이 남음
// 예: 10명이 각각 차감했지만, 업데이트가 덮어씌워져서 마치 1~2명만 차감한 것처럼 보임
assertThat(actualStock).isEqualTo(expectedStock);
}
`
테스트 결과

시나리오 분석:
시간 | 트랜잭션 A (사용자1) | 트랜잭션 B (사용자2)
-----|----------------------------------|----------------------------------
T1 | stockNumber = 100 읽음 |
T2 | | stockNumber = 100 읽음
T3 | restStock = 100 - 2 = 98 계산 |
T4 | | restStock = 100 - 2 = 98 계산
T5 | stockNumber = 98 로 업데이트 |
T6 | | stockNumber = 98 로 업데이트 💥
T7 | COMMIT |
T8 | | COMMIT
결과 | 최종 재고 = 98 (실제로는 96(100-4)이어야 함)
사용자1이 재고를 업데이트했지만 사용자2가 업데이트 이전의 재고 값으로 다시 덮어쓰는 Lost Update 문제가 발생하여 상품 재고의 일치하지 않는 문제가 발생하여 주문시 상품을 조회하는 메서드에 Lock을 걸어 문제를 해결했습니다.
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Override
Optional<Item> findById(Long id);
수정후
## 🔧 해결 방법: 비관적 락 (Pessimistic Lock)
ItemRepository에 `@Lock` 어노테이션을 추가하여 비관적 락을 적용했습니다.
```java
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Override
Optional findById(Long id);
```
**동작 방식:**
- `findById()` 호출 시 해당 상품에 대해 데이터베이스 레벨에서 락 획득 (`SELECT ... FOR UPDATE`)
- 다른 트랜잭션은 락이 해제될 때까지 대기
- 트랜잭션 종료(COMMIT/ROLLBACK) 시 락 자동 해제
**수정 후 흐름:**
```
시간 | 트랜잭션 A | 트랜잭션 B
-----|----------------------------------|----------------------------------
T1 | findById() - 락 획득 🔒 |
T2 | stockNumber = 100 읽음 |
T3 | | findById() - 대기 ⏳
T4 | restStock = 100 - 2 = 98 계산 |
T5 | stockNumber = 98로 업데이트 |
T6 | COMMIT - 락 해제 🔓 |
T7 | | 락 획득 → stockNumber = 98 읽음
T8 | | restStock = 98 - 2 = 96 계산
T9 | | stockNumber = 96로 업데이트
T10 | | COMMIT
결과 | 최종 재고 = 96 ✅ (정확함)

'SW Engineering' 카테고리의 다른 글
| [SW Engineering] 동시성 이슈가 발생하는 이유와 비관적 락을 통해 동시성 문제 해결하기 (0) | 2025.12.24 |
|---|---|
| [SW Engineering] JWT 기반 인증의 한계와 보안·제어성 개선 전략 (0) | 2025.12.20 |
| [SW Engineering] Inflearn 백엔드 애플리케이션 성능 테스트 하기(foo) (0) | 2025.12.17 |
| [SW Engineering] 이커머스 프로젝트 주문내역 조회 N+1 문제 해결 과정 (0) | 2025.10.21 |
| [SW Engineering] N+1 문제 해결: Map 캐싱을 이용한 상품 데이터베이스 조회 최적화 사례 (0) | 2025.10.05 |
Comments
