🦐 모놀리식 상황에서 재고 처리 동시성 문제
먼저 동시성을 해결해야 하는 문제가 발생했다 바로 주문을 할때 여러명이 동시에 같은 물건을 주문한 상황이 발생한 것이다.
아 물론 발생한건 아니고 그런 상황에서 문제가 일어난다는 것이다. 즉, 이번에 제작한 나의 코드를 통해 동시성 문제를 해결해 보려고 한다. 동시성을 해결하기 위한 방법은 해당 블로그에 적혀있다.
https://latewalk.tistory.com/243
💾 동시성 문제를 해결하는 방법
🧊 재고 처리에 대한 동시성 문제동시성 문제는 동일한 데이터에 2개 이상의 스레드, 혹은 세션에서 가변 데이터를 동시에 제어하게 될 때 나타나는 문제이다 만약 하나의 작업이 진행하는 중
latewalk.tistory.com
우선 문제가 일어나는 코드를 살펴보자
Service 코드
테스트 코드
- ExecutorService
- 병렬작업을 할 시에 여러개의 작업을 효율적으로 처리할 수 있다
- ThreadPool을 보다 쉽게 구성하고 실행하는 것도 편리하다
- Executors를 통해 ExecutorService 생성 및 스레드 풀의 갯수 및 종류를 지정해 줄 수 있다
- CountDownLatch
- 특정 스레드가 작업이 완료될 때까지 기다리게 해주는 클래스이다
- 멀티 스레드가 100번의 작업이 모두 완료되면 테스트를 기다리게 한다
- AtomicInteger
- 멀티 스레드 환경에서 안전하게 사용할 수 있는 원자적인 연산을 지원하는 클래스이다.
- Integer 이기 때문에 int 타입의 값을 저장하고 해당 값에 대한 원자적인 연산을 제공하고 경쟁 조건을 방지한다
- 현재 코드에서 카운터의 역할이 되고 성공과 실패의 갯수를 확실히 볼 수 있게 해준다.
- await()
- 100개의 스레드가 모두 하는 일을 마치길 기다리는 역할이다
- Shutdown
- 그후 작업을 종료한다.
- assertThat
- 성공과 실패가 알맞게 조정되어 있는지 확인한다
🦐 각 테스트 및 결과
테스트 조건
한 개의 물건에 100개의 갯수를 저장한다 100명이 동시에 해당 물건을 2개씩 구매한다는 전제하로 테스트가 진행된다.
1. 아무것도 하지 않은 테스트 ( 실패 )
- 동시성 문제가 일어나고 있다는 사실을 여지없이 보여준다
- 문제
- 100개는 모두 동시에 주문을 하는 메소드에 접근한다
- 수정하려는 물건의 갯수를 읽는 것도 거의 동시에 일어나기 때문에 읽는 값도 비슷하다
- 2개가 빠진 갯수를 수정하는 수십개의 메소드는 서로 98이라는 값을 업데이트한다
- 이제 다들 100개의 물건이 확인되기 때문에 주문이 가능한 것이고 성공이 모두 되기 때문에 100으로 잡힌다
- 그렇다면 왜 84일까? 이것은 아마 속도의 차이일 것이다. 위에서 말한것처럼 거의 동시에 라고 했지만 완벽한 동시가 아니기 때문에 성능차이로 인해 가장 늦게 데이터를 읽은 스레드가 값을 수정하는 것이다. 이는 테스트를 누를때마다 변경되는 값으로 인증할 수 있다.
2. Syncronized 도입
현재 나의 서비스는 모놀리식에 한 개의 서버만을 다루고 있는 것을 보여준다. 때문에 가장 먼저 Java에서 제공하는 Syncronized를 도입해보려고 한다
간단하게 설명하자면 해당 메소드를 동기화시키고 동시에 Blocking 시키면서 들어오려는 스레드들을 모두 한 줄 서기 시키는 것과 마찬가지이다. 대충 이해가 갔을것이라 생각하고 서비스 로직을 바꿔보자
놀랍게도 끝났다. 해당 메소드에 synchronized를 붙여서 진행하게 되면서 끝이 난 것이다.
테스트도 안정적으로 끝마친것을 알 수 있다.
결과
목적한대로 성공과 실패가 50번씩 발생하고 남은 물건의 갯수도 0개로 그대로 인 것을 알 수 있다.
- 문제
- 멀티쓰레딩 환경에서 정말 자주 일어날 수 있는 주문에 관한 메소드가 동기화를 통해 한 줄 서기로 통제당하는 것은 성능에 굉장히 좋지 않다는 생각을 하게 된다
- 또한 현재 코드에서 정말 많은 SQL 쿼리 문이 발생하는 것을 알 수 있다. 이를 처리하기 위해 Transactional을 사용하고 ACID 원칙을 사용해서 코드를 짜서 예외가 발생하면 롤백하는 기능 등등 데이터 베이스에서 발생할 수 있는 오류를 잡아줄수 있는 정말 좋은 기능을 사용하지 않는다
※ 참고로 synchronized 는 Transactional 어노테이션과 함께 사용하지 못한다. - 만약 서버가 더 생긴다면?? 모놀리식이 아닌 MSA 방식으로 인해 혹은 모놀리식의 서버를 혹여나 증설했다면 주문을 하는 메소드가 여러개가 생기는 것이다. 하지만 DB는 한 개를 사용하고 있기 때문에 큰 문제가 발생하는 것이다
3. DB Lock 도입
그렇다면 sync의 문제를 DB에서 잡으면 된다. 어차피 DB는 한개를 사용하게 될테니 말이다.
DB에서 데이터를 읽거나 쓰려고 하면 해당 트랜잭션이 DB를 blocking 시키고 자신이 다 사용하면 다시 돌려주는 것이다. 그러면 다음으로 사용하게 될 트랜잭션은 자연스럽게 변화된 데이터를 읽기 때문에 동시성 문제가 발생하지 않는것이다
우선 코드에 들어가기 이전에 DB를 Lock하는 방법에는 두가지가 있다
- Optimistic Lock (낙관적 락)
- Pessimistic Lock(비관적 락)
두개의 차이는 충돌을 대비하느냐 안하느냐의 차이이다. 낙관적은 충돌 안나겠지 뭐~ 하는 마음으로 구성하고 비관적은 무조건 무슨일이 날꺼야 라는 생각으로 구성하는 것이다. 나는 앞서 말했듯이 당연히 충돌이 나는 경우이기 때문에 심지어 주문을 하면서 상품의 갯수를 담당하는 상황이기 때문에 충돌이 꼭 날것이라고 생각하면서 만들었기 때문에 비관적 락을 사용해보고자 한다.
비관적 락과 낙관적 락의 가장 큰 특징은 처리하는 곳의 위치이다
- 비관적 락 : Repository 처럼 DB에 접근하는 로직에 @Lock을 건다
- 낙관적 락 : Entity에 version컬럼을 추가시키고 @Version 을 통해 관리한다.
- Repository
- 가장 중요한 코드는 역시 재고 수정에 가장 중요한 로직인 findByProductUUID 이다.
- 기존의 findByProductUUID는 남겨두고 새로운것을 만든다. 여기서 ForUpdate를 붙여 Lock 에사용된다는 것을 명시한다. 여기선 PESSIMISTIC_WRITE 로 해당 트랜잭션이 시작되면 read, write 행위 모두 lock하는 Exclucive Lock을 사용한다.
- 또한 @Query 문을 사용하여 쿼리를 꼭 만들어준다 왜냐하면 @Lock은 해당 엔티티에 어떤 종류의 lock을 걸지 정해주는 것이지 쿼리 자체에서 Lock모드를 지정할 방법이 없는것이다. 따라서 Lock모드를 지정해서 진행하려면 꼭 써줘야한다.
- Service
- @Transactional 을 사용해서 변경되거나 추가되는 쿼리를 동시에 commit 할 수 있게 만들기 때문에 훨씬 더 코드가 깔끔해진다. save가 난발되지 않기 떄문에 훨씬 편해졌다.
- 또한 만약 중간에 실패하게 되면 자연스럽게 안에 있는 모든 쿼리문이 rollback되는 이점도 존재한다.
결과
결과는 성공적으로 진행되었다는 것을 알 수 있다.
- 진행과정을 생각해보자
- 100명의 인원이 주문을 생성하는것 까지는 동시에 진행했지만 findByProductUUID에서 Lock에 걸려 순서대로 진행되기 시작한다. Exclusive Lock이기 때문에 읽는것 조차도 허용되지 않는다. 그 이후로 해당 트랜잭션이 commit 되기 전까지 lock은 유지 된다. 그리고 해당 트랜잭션이 commit 으로 작업을 완료하면 뒤에 있는 트랜잭션이 그제서야 find를 시작하기 때문에 변경된 데이터를 읽는다
- 이는 데이터가 침범되지 않기 때문에 동시성 문제가 일어날 일이 없다. 하지만 syn의 한줄서기와 느낌이 비슷하다 때문에 요청이 많아지면 많아질수록 시간이 굉장히 오래걸린다.
🦐 느낀점
모놀리식의 방식에서 가장 대표적으로 동시성 이슈를 해결하는 방식을 사용해보았다.
Sync를 사용해서 간단하게 동시성을 해결할수는 있었지만 코드가 난잡해지고 혹여 에러가 발생할 수 있는 곳에 대처가 힘들기 때문에 Transactional을 사용해야만 한다고 느끼게 되었고 Transactional을 사용하기 위해서는 lock의 방식을 어떻게 사용해야 할지 많이 고민해보았던 문제가 되었다.
이후 우리의 문제는 서버를 MSA방식으로 모두 나누기 때문에 더이상 Sync는 사용하기 쉽지 않을 것이다. 이후에 MSA로 변경하고어떤 방식으로 해결해야 할지 확인해보아야 할것 같다.
또한 Redis를 이용한 Spin Lock(분산락) 또한 굉장히 좋은 성능을 나타내는 것으로 알고 있다. 이는 추후에 MSA 방식에서 실험하는 것으로 또다시 동시성을 다뤄보고자 한다.
'프로젝트 > 항해99 개인 프로젝트' 카테고리의 다른 글
🚢 Feign Client 와 RestTemplate (0) | 2024.04.30 |
---|---|
🚢 Open Feign을 사용하며 발생하는 서비스 간의 장애 처리 (0) | 2024.04.30 |
🚢 WishList가 장바구니? (Redis 의 Hash타입 사용) (0) | 2024.04.26 |
🚢 이메일 인증 코드, Session 에서 Redis로 (0) | 2024.04.26 |
🚢 JPA 의 AttributeConverter (1) | 2024.04.26 |