오늘은 동시성 제어에 도전해 보았다.
여태까지 구현했던 코드들은 로컬서버에서 나 혼자 구동하기 때문에
딱히 동시성에 대해 생각해보지 않았다.
하지만 최종프로젝트가 다가오면서, 동시성 제어에 도전해보았다.
Counter
라는entity
에쿠폰 개수(count)
가100
으로 가정하고,
쿠폰을 수령하면count
를1 감소
시키는 로직을 구현하였다.
@Transactional public void decreaseCount() { Counter counter = counterRepository.findById(1L).orElseThrow(); counter.setCount(counter.getCount() - 1); counterRepository.save(counter); }
Counter
를 DB에서 불러와서count
의 값을 1 감소시키고- 다시 DB에
Counter
를 저장했다.
@Test @DisplayName("RDBMS를 사용했을 시 동시성 문제가 발생") void concurrencyTest() { System.out.println("\n\n\n\n[concurrencyTest]"); IntStream.range(0, 100).parallel().forEach(i -> counterService.decreaseCount()); counterService.printCount(); }
decreaseCount()
를 반복해서 100번 수행한다면 당연히count
는 100번 감소하여 0이 될 것이다.- 하지만 병렬로 100번을 동시에 수행한다면 과연 count는 0이 될까?
- 동시성 문제로 인해서 0이 아닌 83이 나오는걸 확인할 수 있다.
- 감소된
count
가 저장되기전에 다른곳에서 조회를 해서 문제가 발생하는 것으로 보인다.
- 1번에서 실패한 상황을 조금 바꿔보았다.
- 이번에는
Counter
객체를 5개 만들고count
값이 100인Counter
를 DB에서 불러와서- 삭제하고
"내꺼!"
를 출력하도록 구현해보았다.- 삭제로 한 이유는 삭제를 시도할때 이미 삭제가 되었다면
Exception
이 발생해서 출력이 안될것이라고 예상했기 때문이다.
- 나는 분명 쿠폰을 5개만 준비했지만.. 8명이 받아갔다.
- 심지어 실패했을때 출력하도록한
"까비!"
또한 하나도 출력되지 않았다.- 예상이 빗나간 이유를 검색해 보니,
JPA
에서는 삭제할Entity
가 없어도Exception
을 발생시키지 않는다고 한다. 만약 발생시키고 싶다면 삭제 함수를 오버라이딩해서 예외를 발생하도록 추가해야 한다고 한다!
- 이번에는
Lock
을 통해 동시성 문제를 해결해보았다.public void decreaseCountUsingLock() { RLock lock = redissonClient.getFairLock(LOCK_KEY); try { boolean isLocked = lock.tryLock(10, 60, TimeUnit.SECONDS); if (isLocked) { try { Counter counter = counterRepository.findById(1L).orElseThrow(); counter.setCount(counter.getCount() - 1); counterRepository.save(counter); } finally { lock.unlock(); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }
- 간단히 설명하자면
10초
동안 락이 풀릴때까지 기다리고,60초
가 지나면 락이 자동으로 풀리게 설정하였다.- 물론 함수가 끝나면 바로 락이 풀린다.
Lock
을 걸어서 100개의 시도가 순서대로 처리되어서 성공적으로 쿠폰이0개
남았음을 확인할 수 있었다!
- 앞의
Lock
을 활용할때도Redis
를 사용하였지만,- 이번에는
Redis
가싱글쓰레드
라는 특징을 활용하여 동시성을 제어해보겠다.public void decrementCounter() { ValueOperations<String, String> ops = stringRedisTemplate.opsForValue(); ops.decrement("counter"); }
Redis
가 싱글쓰레드라는 특징을 갖고있어서, 100개의 시도가 순서대로 처리되어서 성공적으로 쿠폰이0개
남았다!
여태까지 동시성 문제에 대해 고려하지 않았었다.
왜냐면 여태까지 진행했던 프로젝트들은 모두 로컬 서버에서 혼자 돌렸기때문에
내가 한명인 이상 동시성 문제가 발생할 수 없고, 정말 천문학적인 트래픽이 아니라면 동시에 처리될 확률이 엄청나게 낮기 때문이다.
그런데 생각해보면 평소에 배달대행앱을 자주 사용하는데, 이러한 선착순 이벤트를 구현한다면 동시성 문제를 반드시 해결해야한다는 생각이 들어서 오늘 실험을 해보았다.
실험을 하면서 가장 충격받은점은, JPA는 삭제에 실패해도 예외를 발생시키지 않는다는 점이다.
평소에 굉장히 편리해서 아주 좋아했던 내 친구 JPA였는데,
이러한 허점이 있다는 사실을 처음 알았다.
오늘 사용했던 Redis
에 대해서는 사실 많이 아는것이 없다..(오늘 처음봤다)
추후에 Redis
와 친해진 뒤에, Redis
에 대한 정리글을 올릴 예정이다!