강의

[재고시스템으로 알아보는 동시성 이슈 해결방법] #2 재고감소 로직 작성 및 테스트

dev_rosieposie 2023. 11. 6. 18:11

들어가기 전 ..

1. 이전 게시글과 이어지는 게시글이므로 프로젝트 환경 및 세팅은 아래 링크에서 확인해주세요!

2. 전체 코드는 아래의 Git에서 확인 가능합니다!

참고하면 좋을 이전 글

 

[재고시스템으로 알아보는 동시성 이슈 해결방법] #1 개발환경 및 프로젝트 구조

들어가기 전 1. 매일 정리해야지 하며 미뤄두었던, 동시성 이슈 해결방법 강의를 정리하려 한다. 2. 동시성 이슈를 고려한 개발을 해보지 않아서 나한텐 흥미로운 접근방식의 강의였다. 3. 책에서

dev-rosiepoise.tistory.com

 

 

GitHub - dev-rosieposie128/stock: 재고시스템으로 알아보는 동시성이슈 해결방법

재고시스템으로 알아보는 동시성이슈 해결방법. Contribute to dev-rosieposie128/stock development by creating an account on GitHub.

github.com

 

 

목표

  • 재고감소를 위한 엔티디와 비지니스로직, curd로직을 작성하자
  • 해당 재고감소코드를 테스트할 테스트코드를 작성해보자
  • 해당 코드가 가지고오는 문제점이 무엇인지 이해하자

재고감소 코드

Stock class

package com.example.stock.domain;


import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

//entity는 database의 테이블이라고 생각
@Entity
public class Stock {

    // id, productId, quantity
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Long productId;
    private Long quantity;

    public Stock() {
    }

    public Stock(Long productId, Long quantity) {
        this.productId = productId;
        this.quantity = quantity;
    }

    //test case를 위한 수량 확인
    public Long getQuantity() {
        return quantity;
    }

    // 재고 감소 - 수량
    public void decrease(Long quantity){
        if(this.quantity - quantity < 0){
            throw new RuntimeException("0개 미만");
        }
        this.quantity = this.quantity-quantity;
    }
}
  • domain 패키지 밑 위치
  • @Entity를 적용하여 JPA를 통해 테이블과 매핑한다. - 기본 생성자 필수 
  • 재고감소 메소드, 수량 확인을 위한 getQauntity() 메소드

StockService class

@Service
public class StockService {

    //stock에 대한 crud가 가능해야하므로, StockRespository를 필드로 가진다

    private StockRepository stockRepository;

    public StockService(StockRepository stockRepository) {
        this.stockRepository = stockRepository;
    }

    @Transactional
    public void decrease(Long id, Long quantity){

        /*  1 재고 확인
            2 재고 감소
            3 갱신 값 저장
         */
        Stock stock = stockRepository.findById(id).orElseThrow();

        stock.decrease(quantity);

        stockRepository.saveAndFlush(stock);
    }
}
  •  재고감소 메소드

StockRepository class

// 재고에 대한 crud
public interface StockRepository extends JpaRepository<Stock, Long> {

}
  • jpa를 상속받은 StockRepository

StockApplicationTests class

@SpringBootTest
class StockApplicationTests {

   @Autowired
   private StockService stockService;
   @Autowired
   private StockRepository stockRepository;

   // 테스트 전에 데이터베이스에 재고가 있어야 하므로 데이터 생성
   @BeforeEach
   public void before(){
      stockRepository.saveAndFlush(new Stock(1L,100L));
   }
   // 테스트 후 데이터 삭제
   @AfterEach
   public void after(){
      stockRepository.deleteAll();
   }

   @Test
   public void 재고감소() {
      stockService.decrease(1L, 1L);

      Stock stock = stockRepository.findById(1L).orElseThrow();

      assertEquals(99, stock.getQuantity());
   }
}
  • @BeforeEach 와 @AfterEach를 통해 데이터베이스에 재고를 생성하고 삭제한다
  • @Test 재고감소 코드 작성
    • 1L의 아이디를 가진 재고를 1L만큼 감소 시킨다 [ 현재재고 100-1 = 99 (기대값) ]

결과

=> 테스트 통과

 

왜냐하면, 단일 서버 환경 기준이기 때문 그렇다면 이 코드로 작성했을 때 다중서버에서 문제는?

 

@Test
public void 동시에_100개의_요청() throws InterruptedException {

   // 100개의 요청을 보낼 것임
   int threadCount = 100;

   // 멀티 스레드를 이용하기 위해 ExecutorService 사용 - (비동기로 실행하는 작업을 단순화하여 사용하게 하는 자바 api)
   ExecutorService executorService = Executors.newFixedThreadPool(32);
   CountDownLatch latch = new CountDownLatch(threadCount);
   /*
    1OO개의 요청이 모두 끝날때까지 기다려야 하므로 CountDownLatch 사용
    CountDownLatch는 다른 스레드에서 수행죽인 작업이 완료될 때까지 대기할 수 있도록 도와주는 클래스 */

   for(int i=0; i< threadCount; i++){
      executorService.submit(()->{

         try{
            stockService.decrease(1L, 1L);
         }finally {
            latch.countDown();
         }
      });
   }

   latch.await();

   Stock stock = stockRepository.findById(1L).orElseThrow();

   // 100 - (1*00) = 0 : 예상 값
   assertEquals(0, stock.getQuantity());
}
  • 다중 서버를 구현하기 위해 멀티스레드 사용 
  • 100개의 요청 

결과

=> 실패. 기대값은 0이었으나 96임

 

왜 그럴까?

이유 : 레이스 컨디션이 일어났기 때문

 

레이스 컨디션이란?

둘 이상의 Thread가 공유 데이터에 액세스 할 수 있고 동시에 변경을 하려고할 때 발생하는 문제 

 

예시를 살펴보면,

  1. 우리는 Thread1이 데이터를 가져가서 갱신을 한 값을 Thread2가 가져간 이후에 갱신을 하는 것을 예상함
  2. 하지만, 실제로는 Thread1이 데이터를 가져가서 갱신을 하기 이전에 Thread2가 가져가서 갱신되기 전의 값을 가져가게 됨
  3. 그리고 Thread1이 갱신을 하고 Thread2도 갱신을 하지만 둘다 재고가 5인 상태에서 1을 줄인 값을 갱신하기 때문에 갱신이 누락되게 됨

=> 이렇게 두개 이상의 스레드가 공유 데이터에 엑세스 할 수 있고 동시에 변경을 하려고 할 때 발생하는 문제를 레이스 컨디션이라고 함

 

그럼 어떻게 해결을 해야할까?

key point ! 하나의 스레드가 작업이 완료된 이후에 다른 스레드가 데이터에 접근할 수 있도록 하게 하면 된다.

 

 

문제를 해결할 수 있는 해결 방법은 여러가지가 있고,, 투비 컨티뉴 ...!!

 

 

강의 참고

https://www.inflearn.com/course/%EB%8F%99%EC%8B%9C%EC%84%B1%EC%9D%B4%EC%8A%88-%EC%9E%AC%EA%B3%A0%EC%8B%9C%EC%8A%A4%ED%85%9C/dashboard

 

재고시스템으로 알아보는 동시성이슈 해결방법 - 인프런 | 강의

동시성 이슈란 무엇인지 알아보고 처리하는 방법들을 학습합니다., 동시성 이슈 처리도 자신있게! 간단한 재고 시스템으로 차근차근 배워보세요. 백엔드 개발자라면 꼭 알아야 할 동시성 이슈

www.inflearn.com