Spring, JPA, MySQL을 활용한 주문 서비스 동시성 제어
이번 시간에는 비관적 락과 락 미사용 방식 비교 분석하려고 한다.
JUnit 테스트 : 각 요청수에 따른 동시성 처리 비교
JMeter 테스트 : 600명 동시 요청 기준으로 비관적 락 적용 여부에 따른 성능 비교
JUnit 테스트
1000명 사용자를 기준으로 주문요청에 대한 동시성 테스트를 작성했다.
1000명 중 1명만 주문 성공하는지 테스트 - 락 없이
@Test @DisplayName("1000명 중 1명만 주문 성공하는지 테스트 - 락 없이") void concurrentOrder_shouldSucceedForOnlyOneUser() throws InterruptedException, IOException { // given User uploader = getUserDto("uploader"); Long itemId = uploadItem(uploader); int threadCount = 1000; ExecutorService executor = Executors.newFixedThreadPool(threadCount); CountDownLatch latch = new CountDownLatch(threadCount); List<User> buyers = IntStream.range(0, threadCount) .mapToObj(i -> getUserDto("buyer" + i)) .collect(Collectors.toList()); // when for (int i = 0; i < threadCount; i++) { final int index = i; executor.submit(() -> { try { saleItemService.order(buyers.get(index).getEmail(), itemId); log.info("✅ 주문 성공: {}", buyers.get(index).getEmail()); } catch (Exception e) { log.warn("❌ 주문 실패: {}", buyers.get(index).getEmail(), e); } finally { latch.countDown(); } }); } latch.await(); executor.shutdown(); // then List<SaleItemResponse> sameItemSales = saleItemService.getSaleItems(itemId); log.info("DB 기준 같은 아이템 주문 수: {}", sameItemSales.size()); sameItemSales.forEach(sale -> log.info("성공한 구매자: {}", sale.getBuyerEmail())); assertEquals(1, sameItemSales.size(), "DB에도 해당 아이템에 대한 주문은 1건만 존재해야 합니다"); List<SaleItemResponse> all = saleItemService.getAll(); for (SaleItemResponse saleItem : all) { log.info("order info user= {}, itemName= {}", saleItem.getBuyerEmail(), saleItem.getNameKor()); } }1000명 중 1명만 주문 성공하는지 테스트 - 비관적 락 적용
@Test @DisplayName("동시 거래 요청 - 1000명 중 1명 성공, 나머지는 실패") void concurrentContactTrade_success_withTenBuyers() throws InterruptedException, IOException { // given User uploader = getUserDto("uploader"); Long itemId = uploadItem(uploader); Item item = itemRepository.findById(itemId).orElseThrow(ItemNotFoundException::new); int threadCount = 1000; AtomicInteger failCount = new AtomicInteger(0); // 실패 카운터 ExecutorService executorService = Executors.newFixedThreadPool(threadCount); CountDownLatch startLatch = new CountDownLatch(1); CountDownLatch endLatch = new CountDownLatch(threadCount); // 10명의 구매자 생성 List<User> buyers = new ArrayList<>(); for (int i = 0; i < threadCount; i++) { buyers.add(getUserDto("buyer" + i)); } // when for (int i = 0; i < threadCount; i++) { final int index = i; executorService.submit(() -> { try { startLatch.await(); // 모든 스레드 동시에 시작 saleItemService.contactTrade(buyers.get(index).getEmail(), item.getId()); } catch (Exception e) { failCount.incrementAndGet(); } finally { endLatch.countDown(); } }); } startLatch.countDown(); // 모든 스레드 시작 endLatch.await(); // 모든 스레드 종료 대기 // then log.info("실패 요청 수 ={}", failCount.get()); assertEquals(threadCount - 1, failCount.get()); // 1명만 성공해야 함 executorService.shutdown(); }JUnit 동시성 테스트 결과
처리 방식 요청 (사용자 인원) 응답 시간 성공 여부 비관적 락 사용 10 3초 166ms 1명 성공 (정상) 미사용 10 3초 131ms 1명 성공 (정상) 비관적 락 사용 100 3초 994ms 1명 성공 (정상) 미사용 100 5초 84ms 1명 성공 (정상) 비관적 락 사용 1000 12초 946ms 1명 성공 (정상) 미사용 1000 12초 946ms 1명 성공 (정상) 락 미사용 시
- 요청 수와 관계없이 테스트가 성공함.
- 추정 원인: Item과 SaleItem 간의 연관관계(SaleItem이 Item의 FK를 가짐)로 인해, 트랜잭션 커밋 전까지 상태 반영이 지연되어 동시 접근 제어가 발생한 것으로 보임.
비관적 락 사용 시
응답시간은 증가하나 데이터 충돌 없이 순차 처리가 안정적으로 보장됨
JMeter 테스트
데이터 넣기
테스트 용도로 사용할 사용자 데이터 1000명을 생성한다.
DELIMITER $$ DROP PROCEDURE IF EXISTS INSERT_USER_TEST_DATA_BATCH$$ CREATE PROCEDURE INSERT_USER_TEST_DATA_BATCH() BEGIN DECLARE i INT DEFAULT 0; DECLARE base_id INT DEFAULT 1000; -- 시작 ID WHILE i < 1000 DO -- Wish List Insert INSERT INTO wish_list() VALUES (); SET @wish_id = LAST_INSERT_ID(); -- User Base Insert INSERT INTO user_base (user_id, email, password, user_level, dtype) VALUES ( base_id + i, CONCAT('amy', i + 1, '@gmail.com'), '12341234', 'USER', 'USER' ); -- Users Insert INSERT INTO users (user_id, nickname, profile_img, roles, user_status, wish_list_id) VALUES ( base_id + i, CONCAT('amy', i + 1), 'https://github.com/user-attachments/assets/504d8b94-9cac-4a08-a47f-126c8c861ffd', NULL, 'NORMAL', @wish_id ); SET i = i + 1; END WHILE; END$$ DELIMITER ; -- 프로시저 실행 CALL INSERT_USER_TEST_DATA_BATCH();연관관계 설정 때문에 wish_list, user_base, users 테이블에 삽입했다.
비관적 락 - Graph Result
JMeter 동시성 테스트 결과 (사용자 인원 600명으로 진행)
항목 비관적 락 사용 미사용 평균 응답 시간 310ms 1059ms 최대 응답 시간 2,538ms 1,892ms 표준 편차 136.39ms 427.20ms 에러율 99.83% 99.83% Throughput (TPS) 927.4 req/sec 269.3 req/sec - 성공률 : 단일 상품이므로 1건만 성공하고 나머지 599건은 실패하는 것이 정상적인 동작.
- 평균 응답시간 : 락 미사용 시, 비관적 락 사용 대비 3배 이상 증가
- 처리량 : 락 사용 시, 실패 응답도 빠르게 처리되어 전체 처리량이 3배 이상 높음
✅ 결과 및 의견
비관적 락 사용 시, 처리량 높고 응답시간 안정적이며 응답 분산도 적다.
동시성 충돌 상황에서 안정적이며 처리량과 응답 속도 면에서도 우수하기 때문에 비관적 락을 선택함.
'프로젝트 > 개인 프로젝트 V3' 카테고리의 다른 글
| 인덱스를 활용한 쿼리 튜닝과 No OFFSET 방식으로 구조를 개선 (1) | 2025.06.16 |
|---|---|
| Elasticsearch vs MySQL FullText 인덱싱 성능 비교 테스트 (0) | 2025.03.23 |
| Full Text Search를 이용한 DB 성능 개선 (0) | 2025.03.06 |
| 검색 쿼리 리팩토링 (1) | 2024.12.10 |
| 거래요청에 대한 동시성 테스트 (2) | 2024.12.06 |
댓글