본문 바로가기
프로젝트/개인 프로젝트 V3

동시성 테스트, Apache JMeter를 이용한 부하 테스트

by Thumper 2025. 6. 16.

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

    image


    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배 이상 높음

    ✅ 결과 및 의견

    비관적 락 사용 시, 처리량 높고 응답시간 안정적이며 응답 분산도 적다.
    동시성 충돌 상황에서 안정적이며 처리량과 응답 속도 면에서도 우수하기 때문에 비관적 락을 선택함.

댓글