데블 아니고 데블리

운동,햄버거, 개발 좋아요

🐷💻📝

항해99 취업 리부트 코스 학습일지

[항해99 취업 리부트 코스 학습일지] 2024.05.06.(월) WIL 동시성처리 (1)

데블아니고데블리 2024. 5. 7. 00:40

WIL 이라고 하고 TIL 몰아쓰기라고 생각된다

TIL을 쓸 기회가 있었는데, 내용 정리가 명확하지 않아 WIL로 정리하면 좋겠다는 생각이 들었다

1편은 왜 해야 하는지(목적)을 자세하게 써 보고 2편에는 그에 따른 적용, 결과, 3편에서는 시간, 처리에 대해 작성해보면 좋을 것 같다

[동시성 제어]

여러 명이 같은 자원을 사용할 때 일어나는 문제를 관리하는 것

지금 하고 있는 예약 - 구매로 예시를 든다면

수량이 10개 있는데, 사용자가 10000명이 동시에 들어온다면 10개만 처리되어야 하는 것(너무 당연하죠?)

[그럼 동시성 제어 왜 하지?]

사실.. 스프링이 해주는 것 아닌가? 사용자가 몰려도 트렌젝션으로 묶여있으니까 요청 하나당 한건, 요청 10000건이면 순서대로 처리되는 것 아닌가? 라는 생각을 했습니다. 

테스트를 하기 전 엄청난 많은 요청을 보내야 하기 때문에 테스트 도구인 Jmeter가 필요합니다. 설치 방법과 간단한 세팅 방법은

https://devdevleyy.tistory.com/46 여기에 정리해 두었어요💪🏻

 

[테스트] Jmeter 사용하기 (with 동시성제어 , HttpMediaTypeNotSupportedException 해결)

[Jmeter]아파치(Apache)에서 개발한 성능 테스트 도구Java 기반으로 동작해 GUI 를 통해 테스트 시나리오를 작성하고 테스트 결과를 시각적으로 볼 수 있다다양한 프로토콜과 분산 테스트를 지원한다

devdevleyy.tistory.com

설치가 끝났으면 간단한 API를 만들어 줍니다

 

사용자 id와 수량을 body 에 담아서 보내줄 건데요

 

간단하게만 짜 보았습니다. 재고가 0이 될 때 까지만 반복이 되고, 히스토리 테이블에 저장할 수 있는 로직입니다.

@Transactional
    public void newOrderAdd(NewOrderSaveDto dto) {
        System.out.println("요청 : " + dto.getUserId());

        Items items = itemsRepository.findById(1L).orElse(null);
        int totalAmount = dto.getStock();  // 주문 수량 가져오기

        if(items.getStock() > 0) {
            items.setStock(items.getStock() - totalAmount);
            itemsRepository.save(items);

            Items log = Items.builder()
                    .userId(dto.getUserId())
                    .productId(12)
                    .stock(items.getStock() - totalAmount)
                    .build();
            itemsRepository.save(log);
        }
        System.out.println("남은 재고 : " + items.getStock());
    }

 

 

로그 올라가는 속도 체험하기

결과도.. 엄청나요.. 재고가 7개인 상황을 보면 ?  왜 3번이나 예약이 된거죠?

엄청난 문제죠.. 오버부킹.. 재고가 10건인 상황에서 딱 10건만 성공시켜야지! 생각을 했는데 각 사용자가 동시에 메소드를 호출하여 재고를 감소시키다 보니, 재고 1건이 남았을 때 이미 10명이 결재가 성공한 것 처럼 보이는 크나큰 오류.. 가 생기는 것을 볼 수 있습니다..

+-------+-----------------+-----+-------+
|item_id|product_detail_id|stock|user_id|
+-------+-----------------+-----+-------+
|1      |12               |10   |null   |
|81392  |12               |8    |1      |
|81393  |12               |7    |3      |
|81394  |12               |7    |5      |
|81395  |12               |7    |2      |
|81396  |12               |6    |1      |
|81397  |12               |6    |4      |
|81398  |12               |6    |12     |
|81399  |12               |6    |9      |
|81400  |12               |6    |6      |
|81401  |12               |5    |7      |
|81402  |12               |5    |10     |
|81403  |12               |6    |8      |
|81404  |12               |5    |11     |
|81405  |12               |5    |15     |
|81406  |12               |5    |16     |
|81407  |12               |5    |13     |
|81408  |12               |5    |14     |
|81409  |12               |4    |19     |
|81410  |12               |4    |17     |
|81411  |12               |4    |18     |
|81412  |12               |4    |22     |
|81413  |12               |4    |20     |
|81414  |12               |4    |21     |
|81415  |12               |3    |24     |
|81416  |12               |3    |26     |
|81417  |12               |3    |23     |
|81418  |12               |3    |32     |
|81419  |12               |3    |27     |
|81420  |12               |3    |30     |
|81421  |12               |3    |31     |
|81422  |12               |3    |29     |
|81423  |12               |3    |28     |
|81424  |12               |2    |34     |
|81425  |12               |2    |35     |
|81426  |12               |2    |36     |
|81427  |12               |2    |40     |
|81428  |12               |2    |25     |
|81429  |12               |2    |44     |
|81430  |12               |2    |33     |
|81431  |12               |2    |38     |
|81432  |12               |2    |37     |
|81433  |12               |2    |42     |
|81434  |12               |1    |41     |
|81435  |12               |1    |133    |
|81436  |12               |1    |45     |
|81437  |12               |1    |209    |
|81438  |12               |1    |193    |
|81439  |12               |1    |245    |
|81440  |12               |1    |54     |
|81441  |12               |1    |48     |
|81442  |12               |1    |49     |
|81443  |12               |1    |46     |
|81444  |12               |0    |52     |
|81445  |12               |0    |56     |
|81446  |12               |0    |55     |
|81447  |12               |0    |57     |
|81448  |12               |0    |50     |
|81449  |12               |0    |61     |
|81450  |12               |0    |51     |
|81451  |12               |0    |59     |
|81452  |12               |0    |58     |
|81453  |12               |0    |60     |
+-------+-----------------+-----+-------+

 

인기있는 상품을 노리는 한국인의 스피드는.. 어마무시했구요.. 결과는 처참했습니다.

이런 상황은 절때 절때 절때 발생하면 안되겠죠..

왜 이런 결과가 발생했을까요? [레이스 컨디션] 이 발생했기 때문인데요, 

레이스 컨디션은 둘 이상의 쓰레드(Thread)가 공유 자원(재고)에 접근하여 동시에 변경을 할 때 발생하는 문제점입니다

재고가 10개인 상품에 3명의 요청이 들어가면 일반적으로 10 - 3 = 7 , 총 7개가 남아있기를 기대하고 테스트를 진행합니다.

그렇지만 우리의 쓰레드는 생각처럼 처리해 주지는 않죠..

쓰레드 1번, 2번, 3번이 동시에 요청이 들어온거라면 모두 재고가 10개인 시점이였던 것입니다.... 그래서 최종적으로는 재고가 1개만 줄어든 것이죠.. 이러면 안됩니다 정말로

[어떤 방법으로 처리할까?]

@synchronized 사용하기, 캐싱, lock(DB단에서 막아줌) 등 많은 방법들이 있는데, 이번에는 캐싱과 lock에 대해 공부해 보려고 합니다.

개념부터 잡고 가면 좋을 것 같습니다.

 

[Lock 개념 잡기]

1. 비관적 락 (Pessimistic Lock)

  • 데이터에 대한 접근을 기다리는 것을 기본으로 가정
  • 데이터를 변경하기 전에 항상 락을 얻으려고 시도하며, 다른 스레드나 프로세스가 데이터를 변경하는 것을 막는다
  • 트랜잭션 단위로 제어,자원을 점유하는 동안 데이터에 대한 락을 설정하여 다른 트랜잭션에서 데이터를 변경하지 못하도록 한다

2. 낙관적 락(Optimistic Lock):

  • 낙관적 락은 데이터에 대한 변경 충돌이 발생할 것으로 예상하지 않고, 데이터를 변경하기 전에 락을 설정하지 않는다 
  • 대신, 데이터를 읽을 때 버전 번호나 타임스탬프와 같은 메타데이터를 함께 읽고
  • 데이터를 변경할 때 이 메타데이터와 함께 데이터를 업데이트하며, 업데이트가 충돌하지 않으면 변경을 반영
  • 현재 프로젝트 목적(수량표기의 정확도)랑은 맞지 않아 구현에서는 패스

3. 분산락(Distributed Lock)

  • 분산 락은 여러 서버 또는 프로세스 간에 공유된 자원에 대한 동시성 제어를 관리하는 lock, 설명부터 동시성 제어라고 나오네요.
  • 대표적으로 ZooKeeper나 Redis와 같은 분산 환경에서 제공되는 서비스를 사용하여 구현
  • Redis의 경우 Lettuce, Redisson 이 있고
    • Lecttuce
      • 사용하기 편리하다
      • 스핀 락, SETNX라는 명령어를 사용해서 Redis에 락 획득 요청을 보내야 한다
      • 때문에 Redis에 많은 부하를 가한다
    • Redisson
      • pub/sub방식을 사용해 Redis에 가해질 수 있는 부하를 줄일 수 있음
      • 특정 Channel 에 해제 되었다는 이벤트를 발행하면 구독하는 consumer 들이 lock 을 획득하는 방식
      • 러닝커브 있음

이런 차이점이 있네요

 

[Queue 방식, 캐싱 개념잡기]

대기열(Queue)를 사용하여 작업을 순차적으로 처리하는 것

1. 이벤트 브로커

  • 동기적
  • 이벤트 발행(publish) 구독(subscribe, 사용자) 시스템이 기반
  • 하나의 이벤트가 여러개 구독자 처리 가능하지만, 하나의 구독자는 하나의 이벤트만 처리 가능(수량을 각 1개씩 할당한다)
  • redis 사용

2. 메세지브로커

  • 미들웨어, 메세지 중계, 관리
  • 비동기적
  • 메세지는 한번에 하나씩 구독자에게 전달, 메세지 브로커를 통해 순차적으로 처리된다
  • rabbitMQ

여기서 엄청난 고민입니다... 메세지브로커 사용할 경우 결제가 완료가 되었다면 DB 작업 기다리지 않고 바로 완료처리 할 수 있는데 수량반영을 해야하면 정확도가 떨어진단 말입니다... 하지만 이벤트 브로커 사용하면 수량에 대해서는 정확하게 표현이 될 수 있겠죠..?

 

1편까지는 원인과 이유, 해결 방법등을 다뤘다면

2편에서는 락

3편에서는 캐싱에 대해서 알아보도록 하겠습니다~