[JPA, Redis, Lock] 동시성 제어를 위한 LOCK 기능 테스트와 Redis 분산락 적용
0. 미리 보는 결론
- 도서 주문 서비스에서 주문 폭주 상황에서 동시성 제어를 하기 위해 Lock기능을 도입했다.
- 현재 단일 서버 상황에서 부정락 기능만 이용해도 충분히 동시성 제어가 가능했지만 추후 다중 서버로 확장까지 고려하여 Redisson 분산 락을 도입했다.
1. Lock 옵션에 따른 실험 결과
1)Lock 옵션 종류와 설명
ㄱ. Optimistic
- NONE : 별도의 옵션을 사용하지 않아도 Entity에 @Version이 적용된 필드만 있으면 낙관적 잠금이 적용됩니다.
- OPTIMISTIC(read) : Entity 수정시에만 발생하는 낙관적 잠금이 읽기 시에도 발생하도록 설정합니다.
읽기시에도 버전을 체크하고 트랜잭션이 종료될 때까지 다른 트랜잭션에서 변경하지 않음을 보장합니다.- 이를 통해 diry read와 non-repeatable read를 방지
- OPTIMISTIC_FORCE_INCREMENT(write) : 낙관적 잠금을 사용하면서 버전 정보를 강제로 증가시키는 옵션
ㄴ. Pessimistic
- PESSIMISTIC_READ : dirty read가 발생하지 않을 때마다 공유 잠금(Shared Lock)을 획득하고 데이터가 UPDATE, DELETE 되는 것을 방지 할 수 있습니다.
- PESSIMISTIC_WRITE : 배타적 잠금(Exclusive Lock)을 획득하고 데이터를 다른 트랜잭션에서 READ, UPDATE, DELETE 하는것을 방지 할 수 있습니다.
- PESSIMISTIC_FORCE_INCREMENT : 이 잠금은 PESSIMISTIC_WRITE와 유사하게 작동 하지만 @Version이 지정된 Entity와 협력하기 위해 도입되어 PESSIMISTIC_FORCE_INCREMENT 잠금을 획득할 시 버전이 업데이트 됩니다.
2)실험 결과 요약
- 현재 서비스에서는 PESSIMISTIC_WRITE만 제대로 작동한다.
- Optimistic은 성능은 좋지만 주문이 폭주하는 경우 트랜잭션을 시작할 때 저장한 버전 값과 트랜잭션이 끝나고 난 후 체크한 버전값이 다른 경우가 많은데 이 때 잘 작동하지 않는다. 따라서 진행하는 서비스에는 적합하지 않다.
- 또한 Force_Increment 옵션의 경우 지원하는 DB의 종류가 정해져있는데, 현재 사용중인 mysql에서는 사용이 불가능하다.
- PESSIMISTIC_READ는 어떤 트랜잭션이 수정중일 때 다른 트랜잭션에서 읽기는 가능하다. 하지만 현재 실험에서는 모든 트랜잭션들이 수정하고자 하기 때문에 적합하지 않다.
- PESSIMISTIC_WRITE를 사용한 결과

2. Redisson Distribution Lock
- Transaction이 Lock을 점거하고 있다는 정보를 redis 서버(캐시 서버)에 올려서, 분산된 서버에서 하나의 DB를 조회하고 수정할 때 사용하기에 적절한 방법이다.
- 아래 자세히 설명하겠지만, 요청 Thread를 순서대로 처리한다는 보장이 없다는 단점이 있다.
1)실험 결과 요약
- 잘 작동하지만 아래와 같이 주문 id가 순서대로 되어있지 않다. 즉 요청된 Thread를 순서대로 처리하지 않는다.
- 이러한 단점을 해결하기 위해선 Redisson Distribution Lock 이 아니라 Redisson Fair Lock을 사용해한다.
- 하지만 이 경우에는 DeadLock 문제가 발생할 가능성이 있다.
- 순서대로 처리하지 않는 이유는 먼저 Thread가 lock을 선점한 동안 여러개의 Thread가 대기중일 때, 먼저 선점한 Thread가 트랜잭션을 완료 한 이후 어떤 Thread가 lock을 점유할지 모르기 때문이다.
- Thread1번이 3초가 걸리는 Transaction을 진행중이다.
- 이때 Thread 2번 부터 10번까지 대기를 한다.
- 2~10번까지 Thread1 이 끝날때 까지 계속 lock 점거를 요청한다.
- 2~10번이 계속 요청하기 때문에 1번 종료 시점에 누가 점유하게 될지 장담할 수 없다.

3. 결론
1)JPA의 긍정락과 부정락
- 서비스의 설계에 따라 다르겠지만
- 우선 긍정락의 경우 충돌 시 애플리케이션 단에서 롤백을 직접 해줘야한다. 해당 부분을 구현하지 않았으니 당연하게도 긍정락으로는 현재 서비스에서 동시성 제어가 불가능하다.
- 부정락의 경우 read가 아닌 Write 상황이 폭주하니, 당연하게도 PESSIMISTIC_WRITE에서만 제대로 동시성 제어가 가능했다.
2)Redis 분산락
- 왜 JPA의 기능만으로도 충분한 상황에서 Redis 분산락까지 적용했는가?
- 다중 서버 상황에 대비해서 커뮤니케이션 허브로써 Redis를 활용하는 Lock을 적용하였다.
- 물론 다중서버에서도 DB는 하나여서 Lock을 이용해 제어할 수 도 있지만, 현재와 달리 만약 사용자의 포인트, 현금 보유, 쿠폰과 같이 다양한 정보 테이블과 연동해야하는 상황에서 다중서버 상황까지 겹친다면 문제가 발생할 수 있다.
- 그래서 주문 처리 단계를 Redis 분산 락에 의존하여 모든 주문 중에서 동시에 하나만 처리될 수 있도록 개발하였다.
3)더 나은 방법
- Redis 분산락은 요청 순서를 보장해서 처리해주지 않는 다는 문제가 있다.
- 따라서 메시지큐 기술을 이용해서 처리했어야 마땅하다고 할 수 있다.
- 하지만 이번 프로젝트의 의의는 단순히 고성능 처리보다 적용할 수 있는 기능들을 순차적으로 학습하며 배워가는 것에 초점이 있었다. 이로 인해 메시지큐 적용까지는 시간이 부족하여 프로젝트에 적용한 기술은 Redis 분산락으로 마무리 하였다.
4. 참조
1)mysql 데이터 테이블 기반 테스트 코드 작성하기 : https://dev-coco.tistory.com/85
[Spring Boot] MySQL & JPA 연동 및 테스트 (Gradle 프로젝트)
SpringBoot에서 MySQL 그리고 Spring Data JPA를 연동하는 방법에 대해 알아보도록 하겠습니다. 1. 프로젝트에 의존성 추가하기 build.gradle에 의존성을 아래와 같이 추가해줍니다. dependencies { implementation 'my
dev-coco.tistory.com
2)LockModeType : https://velog.io/@lsb156/JPA-Optimistic-Lock-Pessimistic-Lock
JPA의 낙관적 잠금(Optimistic Lock), 비관적 잠금(Pessimistic Lock)
요청이 많은 서버에서 여러 트랜잭션이 동시에 같은 데이터에 업데이트를 발생시킬 경우에 일부 요청이 유실되는 경우가 발생하여 장애로 이어질 수 있습니다. 이를 위해 동시 읽기/업데이트
velog.io
3)Redisson분산락 : https://velog.io/@hgs-study/redisson-distributed-lock
Redisson 분산락을 이용한 동시성 제어
Redis 클라이언트인 Redisson 분산락(Distributed Lock)을 이용해서 동시성을 제어하는 포스팅을 진행해봤습니다 (예제 포함)
velog.io