들어가며
전자상거래(E-Commerce), 증권, 은행 등 업무 관련 시스템에서 안정성과 직결되는 부분인 동시성(Concurrency) 제어를 고려하지 않고, 개발하게 되면 데이터 정합성(Consistency)이 중요한 상황에서 여러 가지 이슈가 발생할 수 있다. 다중 스레드(Multi thread) 환경에서 서로 다른 스레드(Thread)가 공유 자원에 접근하여 가변 데이터를 동시에 연산 작업을 수행하는 상황에 데이터 정합성이 보장되지 않아 일관성 있는 데이터 조회가 어려울 수 있다. 이를 해결하기 위해 동기화 작업을 수행해야 하며 별도의 처리가 필요하다.
동시성 제어는 왜 필요한가?
여러 개의 스레드가 공유 자원에 동시에 액세스하여 값이 예상과 다르게 변하여 문제가 발생하는 것을 경쟁 상태(Race condition)라고 한다. 이러한 상황에 데이터의 일관성을 해치는 결과가 나타날 수 있다.
동시성 이슈를 해결하지 않으면 어떠한 상황이 일어나는지 아래의 예시를 통해 자세히 알아보겠다.
[거래내역조회]라는 공유 자원에 100만원이라는 값이 있다고 가정한다.
Thread 1은 예금 100만원을 확인하는 상황에서 Thread 2가 예금 100만원을 확인하고 50만원을 입금하여 총 150만원을 저장한다.
아직 Thread 1은 현재 예금은 100만원인 상태에서 추가로 100만원을 입금하면 150만원 + 100만원 = 250만원이 아닌 총 200만원의 예금이 저장된다.
본 아티클에서는 동시성 환경에서 데이터 정합성을 보장하기 위해 Redis를 활용한 안전하게 Lock을 구현하고, 동시성 제어하는 방법에 대해 살펴보겠다.
2. Redis를 활용한 동시성 이슈 제어하기
멀티 스레드 환경에서 동일한 데이터에 대해 동기화된 처리가 필요하며, 여러 스레드에 공통된 락을 적용해야 한다. 이를 해결하기 위해 분산락(Distributed Lock)을 활용할 수 있다. 여기서, 분산락이란 여러 프로세스 또는 스레드가 공유 자원에 접근할 때 데이터의 정합성을 보장하기 위해 사용하는 기술이다.
Redis의 구조 특성상 명령을 처리할 때 단일 스레드(Single thread)로 작동하여 한번에 하나의 명령만 실행하므로 원자성(Atomic)을 유지한다. 이를 통해 Redis를 활용한 분산락을 구현해 동시성 이슈를 해결 할 수 있다.
2-1. Lettuce와 Redisson의 Lock 획득 방식 차이
Lettuce – 스핀 락(spin lock) 방식
– sentnx 메소드를 활용하여 사용자가 직접 분산락을 구현해야 한다.
– Lock의 timeout이 지정되어 있지 않아 lock을 획득하지 못하면 무한 루프를 돌게될 risk가 크다.
• 일정 시간이 지나면 lock이 만료되도록 구현해야 한다.
• Lock을 획득하는 최대 허용시간 또는 횟수를 정해주는 방법이 있다.
• 만약 lock을 획득하는데 실패하면 연산을 수행할 수 없는 상태이므로 Exception을 던진다.
- Lock의 획득에 실패했을 경우 계속 lock을 점유하려고 시도하기 때문에, 요청이 많을수록 redis가 받는 부하는 커지게 된다.
Redisson – Pub/sub 방식
- redisson-spring-boot-starter 라이브러리 추가
- Lock 구현체의 형태로 분산락을 제공한다.
- Pub/sub 방식을 사용하며, 스핀 락을 사용하지 않는다.
• 대기 없이 tryLock 메소드를 이용해서 lock을 획득에 성공하면 true를 반환한다.
• Lock이 해제되면 lock을 subscribe하는 클라이언트는 해제되었다는 신호를 받고 lock 획득을 시도한다.
• Pub/sub 방식을 사용해 스핀 락 방식에 비해 redis에 지속적으로 lock 획득 요청을 보내는 과정이 사리지고, redis에 부하를 덜 준다는 장점이 있다.
3. 동시성 처리를 위한 Redis의 Redisson 라이브러리 선택
Redisson의 분산락은 스핀 락 방식을 사용하지 않고, Pub/Sub 구조를 사용함으로써 Redis에 발생하는 트래픽을 많이 줄였다.
Lock을 획득하기 위해 tryLock을 시도하는 클라이언트들은 Lock 메시지를 subscribe하고, Lock이 해제되길 대기 중인 클라이언트들에게 Lock 획득을 시도하라는 알림을 주는 방식이며, lock에 대해 timeout과 같은 설정을 지원하므로 lock을 안전하게 사용할 수 있다.
3-1. Redisson 라이브러리를 이용한 분산락 사용법
Redisson은 Lock을 사용하기 위해 RLock이라는 interface를 지원하며 Lock 획득을 위해 ‘tryLock()’을 사용한다.
tryLock 메소드에 lock 관련 time을 명시하도록 되어있다.
- waitTime: Lock 획득을 위해 대기할 timeout, 만약 wait time 만큼 지나면 false가 반환되어 lock 획득에 실패한다.
- leaseTime: Lock이 만료되는 시간이며, leaseTime 만큼의 시간이 지나면 lock이 만료되어 사라지기 때문에, lock을 별도로 해제하지 않아도 다른 스레드 또는 애플리케이션에서 lock을 획득할 수 있다
- TimeUnit: 시간단위
아래의 코드는 Redisson의 Lock 인터페이스 구현 코드 예제이다.
1. “myLock” key라는 이름에 대한 RLock 인스턴스를 가져온다.
2. tryLock 획득을 시도한다. (성공: true, 실패: false)
3. Lock 획득하면 로직을 수행하고, 실패 시 Lock을 subscribe하며 해제되길 기다린다.
4. 마지막으로, finally에서 Lock을 해제한다.
4. 테스트 시나리오 검증 – 동시성 제어
분산락은 여러 서버(또는 프로세스)에서 공유된 자원에 접근할 때, 데이터의 정합성을 제어하기 위해 사용되는 기술이며, 다음과 같은 상황에 사용될 수 있다.
커피 쿠폰 제공 이벤트 상황에서 커피 쿠폰을 발급받고 차감하는 경우, 동시에 요청이 들어오는 공유된 쿠폰 데이터의 일관성을 보장하고 동시성을 제어할 수 있다.
커피 쿠폰(ID) 100매를 선착순으로 고객들에게 이벤트로 발급한다. 115명이 이벤트에 참여하여 발급받는다고 했을 때, 분산락 적용과 미적용 두 가지 케이스(Case)에 대해 테스트코드를 통해 확인한다.
“분산락 적용”
“분산락 미적용”
4-1. 테스트 검증 : 선착순 커피 쿠폰 발급(분산락 미적용)
선착순으로 커피 쿠폰 한정 수량 100매를 제공하여 115명 고객들이 쿠폰을 발급받을 경우, 분산락을 사용하지 않고 테스트를 수행한다.
테스트 결과
커피 쿠폰 총 100매에서 동시에 115명 고객들은 쿠폰을 발급할 경우, 그 중 100명만 쿠폰을 받고 나머지 15명은 쿠폰이 모두 소진되어 쿠폰 발급이 안되는 것을 예상했지만, 쿠폰 잔여 수량은 98매가 남아있었다. Lock이 없다 보니 동시에 요청이 왔을 때, 각 사용자마다 조회한 커피 쿠폰의 잔여 수량이 동기화가 되지 않아서 총 수량 100매였던 쿠폰은 1~2개 밖에 줄지 않았고, 데이터 정합성이 깨진 상황이 발생했다.
4-2. 테트스 검증 – 선착순 커피 쿠폰 발급 (분산락 적용)
이번 테스트는 분산락을 적용하여 동시성 테스트를 검증한다.
테스트코드를 통해 예상한 결과는 선착순 커피 쿠폰 수량 총 100매에 대해 동시에 115명이 쿠폰을 발급받을 경우, 시스템은 쿠폰 한정 수량으로 100명에게만 쿠폰을 제공하고 나머지 고객에게는 커피 쿠폰은 모두 소진되었다는 메시지를 보내는 것이다.
테스트 결과
분산락이 적용된 테스트코드에서는 동시성 제어하는 모습을 보이고 있으며, 커피 쿠폰의 수량이 모두 소진되면서 이후 요청에서는 정상적으로 커피 쿠폰이 모두 소진된 것을 확인할 수 있다.
마치며
동시성 환경에서 Lock 획득 방식의 차이를 알아보았고, Redis를 이용한 분산락을 사용하는 방법 및 적용 유무에 따라 시스템이 어떻게 동작되는지 테스트 코드를 통해 확인하였다.
동시성 제어를 위해 Redis를 사용할 경우, Lettuce는 스핀 락으로 인해 Redis에 부하가 많이 발생하기 때문에 상황에 따라 제한적일 수 있다. Lock 획득에 재시도 없이 실패로 간주한다면 lettuce를 사용할 수 있고, 충돌로 인해 재시도가 필요하다면 redisson의 분산락을 선택할 수 있다.
앞서 서술한 내용은 모든 상황에서 분산락이 최적이라고 할 수는 없다. 프로젝트의 상황에 따른 규모, 인프라, 아키텍처 요구사항 등에 따라 면밀히 검토한 후에 최적의 구현 방법을 결정해야 한다. 이 아티클을 통해 Redis를 활용한 동시성 제어 방법에 대한 이해와 성능에 대한 고민에 도움이 되기를 바란다.
“선착순 커피 쿠폰 발급” 동시성 프로젝트는 GitHub Repository에서 확인할 수 있다.
# References
- https://github.com/redisson/redisson/wiki/8.-Distributed-locks-and-synchronizers
- https://redis.io/docs/manual/patterns/distributed-locks/
- https://en.wikipedia.org/wiki/Spinlock
- https://www.javadoc.io/doc/org.redisson/redisson/3.26.0/org/redisson/api/RedissonClient.html
- https://hyperconnect.github.io/2019/11/15/redis-distributed-lock-1.html
- https://incheol-jung.gitbook.io/docs/q-and-a/spring/redisson-trylock
- https://github.com/redisson/redisson/tree/master/redisson-spring-boot-starter
- https://github.com/Virusuki/Redisson_Distributed_Lock
김남욱 프로
소프트웨어사업부 OSS사업팀
클라우드 및 오픈소스 SW 관련 연구 개발 프로젝트를 수행하였으며, 현재 OSS 기술서비스 및 아키텍처를 담당하고 있습니다.
Register for Download Contents
- 이메일 주소를 제출해 주시면 콘텐츠를 다운로드 받을 수 있으며, 자동으로 뉴스레터 신청 서비스에 가입됩니다.
- 뉴스레터 서비스 가입 거부 시 콘텐츠 다운로드 서비스가 제한될 수 있습니다.
- 파일 다운로드가 되지 않을 경우 s-core_mktg@samsung.com으로 문의해주십시오.