들어가며
안녕하세요!
오늘은 개발자라면 한 번쯤 마주치게 되는 동시성 이슈에 대해 이야기해보려고 합니다.
전자상거래, 증권, 은행 등 업무 관련 시스템에서 안정성과 직결되는 부분인 동시성(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 구현 방법과 동시성 제어하는 방법에 대해 살펴보겠습니다. 
Redis를 활용한 동시성 이슈 제어하기
멀티 스레드 환경에서 동일한 데이터에 대해 동기화된 처리가 필요하며, 여러 스레드에 공통된 락을 적용해야 합니다.
이를 해결하기 위해 분산락(Distributed Lock)을 활용할 수 있어요.
분산락이란 여러 프로세스 또는 스레드가 공유 자원에 접근할 때 데이터의 정합성을 보장하기 위해 사용하는 기술입니다. 
Redis의 구조 특성상 명령을 처리할 때 단일 스레드(Single thread)로 작동하여 한번에 하나의 명령만 실행하므로 원자성(Atomic)을 유지합니다.
이를 통해 Redis를 활용한 분산락을 구현해서 동시성 이슈를 해결할 수 있답니다!
Lettuce와 Redisson의 Lock 획득 방식 차이 
Lettuce - 스핀 락(spin lock) 방식 
•
setnx 메소드를 활용하여 사용자가 직접 분산락을 구현해야 합니다.
•
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에 부하를 덜 준다는 장점이 있습니다! 
동시성 처리를 위한 Redis의 Redisson 라이브러리 선택
Redisson의 분산락은 스핀 락 방식을 사용하지 않고, Pub/Sub 구조를 사용함으로써 Redis에 발생하는 트래픽을 많이 줄였습니다.
Lock을 획득하기 위해 tryLock을 시도하는 클라이언트들은 Lock 메시지를 subscribe하고, Lock이 해제되길 대기 중인 클라이언트들에게 Lock 획득을 시도하라는 알림을 주는 방식이에요.
또한 lock에 대해 timeout과 같은 설정을 지원하므로 lock을 안전하게 사용할 수 있습니다. 
Redisson 라이브러리를 이용한 분산락 사용법 
Redisson은 Lock을 사용하기 위해 RLock이라는 interface를 지원하며 Lock 획득을 위해 tryLock()을 사용해요.
tryLock 메소드 파라미터 
•
waitTime: Lock 획득을 위해 대기할 timeout, 만약 wait time 만큼 지나면 false가 반환되어 lock 획득에 실패해요.
•
leaseTime: Lock이 만료되는 시간이며, leaseTime 만큼의 시간이 지나면 lock이 만료되어 사라지기 때문에, lock을 별도로 해제하지 않아도 다른 스레드 또는 애플리케이션에서 lock을 획득할 수 있어요.
•
TimeUnit: 시간단위
Lock 인터페이스 구현 예제 
1.
"myLock" key라는 이름에 대한 RLock 인스턴스를 가져와요.
2.
tryLock 획득을 시도해요. (성공: true, 실패: false)
3.
Lock 획득하면 로직을 수행하고, 실패 시 Lock을 subscribe하며 해제되길 기다려요.
4.
마지막으로, finally에서 Lock을 해제해요.
테스트 시나리오 검증 - 동시성 제어
분산락은 여러 서버(또는 프로세스)에서 공유된 자원에 접근할 때, 데이터의 정합성을 제어하기 위해 사용되는 기술이에요.
실제 상황 예시
커피 쿠폰 제공 이벤트 상황에서 커피 쿠폰을 발급받고 차감하는 경우, 동시에 요청이 들어오는 공유된 쿠폰 데이터의 일관성을 보장하고 동시성을 제어할 수 있다.
테스트 시나리오 
•
커피 쿠폰(ID) 100매를 선착순으로 고객들에게 이벤트로 발급합니다.
•
115명이 이벤트에 참여하여 발급받는다고 했을 때, 분산락 적용과 미적용 두 가지 케이스(Case)에 대해 테스트코드를 통해 확인한다.
더 자세한 테스트 검증 결과와 구현 코드는 [아래 링크]에서 확인하실 수 있습니다! 