재고 시스템에 동시성 적용하기 (1)
- 국비지원 (스파르타)
- 2025. 3. 19. 23:08
동시성이라는건 무엇일까?
여러 쓰레드가 동시에 접근해서 발생하는 문제라고 한다.
이렇게까지는 알고는 있는데 내가 하고 있는 테스트가 과연 동시성이 맞는걸까?
간단한 코드를 작성하였다.
@Test
public void testConcurrency() throws InterruptedException {
UUID stockId = UUID.randomUUID();
Stock stock = new Stock(stockId, 1000);
StockRepository mockStockRepository = Mockito.mock(StockRepository.class);
Mockito.when(mockStockRepository.findByIdAndDeleteByIsNull(stockId)).thenReturn(Optional.of(stock));
StockService stockService = new StockService(mockStockRepository, null);
ExecutorService executorService = Executors.newFixedThreadPool(1000);
// 두 스레드에서 동시에 재고 차감 시도
for(int i = 0 ; i < 1000 ; i++){
executorService.submit(() -> {
stockService.updateStock(stockId, new UpdateStockRequest(-1)); // 첫 번째 스레드
});
}
executorService.shutdown();
executorService.awaitTermination(1, TimeUnit.SECONDS);
assertEquals(0, stock.getProductQuantity());
}
쓰레드를 1000개를 만들고 반복문을 1000번돌려서 -1씩 감소를 시켰다.
이렇게 되면 0이라는 값이 리턴이 되어야 한다.
하지만 결과는 10번중 7번 성공으로 약 30%정도의 실패율을 보였다.
근데 이게 과연 동시성 문제일까? 머리로는 이게 동시성이라는건 알겠는데 확실하게 동시성 때문에 발생한 문제입니다.라고 말은 못하겠다.
왜나하면 1. 동시성에 대한 지식이 전무하다. 2. 동시성을 다룬적이 없다. 3. 쓰레드도 직접적으로 사용한적이 없다. 등등 요러한 이유들 때문에 동시성이 맞는지 확신이 서지 않았다. 만약 다른 이유라면 동시성이라는건 도대체 어떻게 터드리는거지?
일단 락을 걸지 않는 상태에서
쓰레드 10개로 100번을 테스트를 100번 돌렸을때를 측정해보자.
(쓰레드 갯수와 테스트 횟수는 동일하게 가져간다.)
쓰레드 갯수가 적어서 그런지 통과율이 3%밖에 되지 않았다.
동시성을 제어하는 방법에는 어떤것이 있을까?
가장 간단한 예시가 메소드에 synchronized를 붙이는 방법이다.
public synchronized void update(int quantity) {
this.productQuantity += quantity;
this.updateBy = "수정된 계정 아이디";
validateQuantity();
}
요런식으로 붙이면 동시성제어가 된다고는 하는데 결과는 어떤지 확인해보자.
놀랍게도 전부 성공하였다.
성공은 했지만 여전히 찜찜한건 사실이다. 단순히 메소드에 synchronized붙이는 방법이 최선일까? 이거의 단점은 없는걸까?
gpt한테 물어보자.
1. 성능 저하 (경합 발생)
- synchronized는 내부적으로 모니터 락(monitor lock) 을 사용해 동기화를 보장해.
- 하지만 여러 개의 스레드가 하나의 synchronized 블록에 접근하려 하면 경합(Contention) 이 발생하고, 락을 기다리는 시간이 길어져 성능이 저하될 수 있어.
2. 세밀한 제어가 어려움
- synchronized는 하나의 객체 단위로 락을 걸기 때문에, 세밀한 락 제어가 어려워.
- 예를 들어, 컬렉션의 일부 요소만 보호하고 싶어도 synchronized는 전체 객체에 대해 락을 걸어버려 불필요한 동기화가 발생할 수 있어.
3. 데드락(Deadlock) 위험
- 여러 개의 synchronized 블록을 사용할 때, 스레드가 서로 다른 락을 기다리는 상황이 발생하면 데드락(Deadlock) 이 발생할 수 있어.
4. 공정성(Fairness) 보장 안됨
- synchronized는 락을 얻을 스레드를 직접 제어하지 않아.
- 즉, 어떤 스레드가 락을 얻을지는 JVM의 스케줄러에 의해 결정되며, 특정 스레드가 계속 선택되지 않는 기아 상태(Starvation) 가 발생할 수도 있어.
5. 재진입 비용(Reentrant Cost)
- synchronized는 재진입 가능(reentrant) 락이지만, 재진입할 때마다 JVM이 모니터 락을 관리하는 비용이 발생해.
- 그래서 락을 여러 번 획득하는 구조라면 성능에 더 큰 영향을 줄 수 있어.
하지만 이 방식은 코드단에서 막는 방법이기때문에 DB에서는 발생하지 않는다는 문제를 가지고 있다.
그러면 코드단에서 막는건 의미가 없을거 같다. 그러면 테스트코드에 DB를 추가해서 테스트를 해보는것이 좋을거 같다.
테스트 했더니...
요게 무슨일이람
1도 감소되는게 아니라 100이 그대로네 이게 맞아?
ㅜㅜ
동작하는거 맞나?
아니라는데...
으악... 왜 접근이 안되는건가!!!
생각해보니 DB락을 걸고 정상적으로 되면 되는게 아닌가 생각이 든다.
귀찮은데 DB락만 걸어보자.
DB락에는 크게 2가지가 있다고 한다.
낙관적 락과 비관적 락 두개가 있다고 한다.
비관적 락
- 이 락 같은 경우는 뭔가 기분나쁘게 생각하는 락이라고 생각하면 된다.
그니까 이름에서 부터 알다시피 락을 비관적으로 생각하는 락이다.
그러면 비관적 락을 사용하게 되면 어떤일이 발생할까?
바로 방화벽같은 시스템이 동작하게 되는데 각 쓰레드가 특정 stock에 접근했을때
이 방화벽같은 비관적락이 미리 그것을 막아 처리해준다고 생각하면 된다.
그러면 미리 들어온 쓰레드가 db를 선점하게 되고 데이터가 반영하게 되어진다.
그런데 문제는 stock이 여러개 인 경우에는 문제가 될 수 있다고 한다.
이것을 데드락 현상이라고 하는데 구체적인 구현은 다음장에서 진행하도록 하자.
낙관적 락
이 락은 본인이 직접 한땀한땀 정성스럽게 락을 건다기 보다는 확인하는 락이라고 생각하면된다.
그래서 본인이 직접 확인하기때문에 @version을 넣어서 사용하게 된다.
그러니까 여러 쓰레드가 접근했을때 버전을 보고 본인이 판단하면 되게 된다.
하지만 요청이 과도하게 들어오면 과로사해서 죽어버린다. 그러니까 외부에 맡겨서 처리하는게 아닌가 싶다.
어쩌면 객체는 성격이 본인중심적이거라 생각한다. 남이 대신 해주냐 내가 대신 하냐 차인데
객체는 본인이 하는걸 좋아하나 보다.
'국비지원 (스파르타)' 카테고리의 다른 글
프로젝트에 권한 최큉! (0) | 2025.03.21 |
---|---|
Btree (0) | 2025.03.20 |
Page<T> -> Pagination<T>로 바꾸기 (1) | 2025.03.18 |
프로젝트에 레이어 4 계층 적용기 (2) | 2025.03.17 |
프로젝트에 레코드를 추가해보자. (1) | 2025.03.14 |