들어가면서
어플리케이션에서 락(Lock)은 멀티쓰레드 환경에서 동시에 같은 자원에 접근하는 것을 방지하기 위해 사용됩니다.
단일 서버 환경에서는 synchronized같은 어플리케이션 레벨 락으로 충분하지만,
여러 서버가 동시에 하나의 자원(예: 주문, 결제, 재고관리)을 접근하는 분산 환경에서는 이 방식만으로는 제어가 어렵습니다.
예를 들어 사용자가 주문 버튼을 2번 연속으로 클릭하여 서버에 요청했다는 가정을 하겠습니다.
단일 서버 환경에서는 synchronized같은 어플리케이션 레벨의 락으로 중복실행을 막을 수 있지만,
서버가 여러개면 1번째 요청은 1번 서버, 2번째 요청은 2번 서버로 요청이 갔을 경우
1번 서버와 2번 서버가 서로의 서로의 상태를 모르기 때문에 2번 실행되어 문제가 발생합니다.
이 문제를 해결하기 위해서 가장 먼저 생각나는 방법은 DB Lock(트랜잭션 기반 락)입니다.
DB는 여러 서버에서 공유되기 때문에, 트랜잭션 단위로 락을 걸면 분산 환경에서도 일관성을 확보할 수 있습니다.
하지만 디스크 I/O 기반이라 락 획득 및 해제에 시간이 오래 걸리고,
트랜잭션 충돌이 자주 발생하는 환경에서는 성능 저하와 병목이 발생합니다.
이 한계를 해결하기 위해 등장한 것이 Redis 기반 분산락(Distributed Lock)입니다.
Redis는 메모리 기반으로 동작하기 때문에 락을 빠르게 제어할 수 있고,
분산 서버 환경에서도 하나의 키를 중심으로 락 상태를 일관성 있게 관리할 수 있습니다.
사용방법
Redis를 이용한 Lock 방식은 주로 2가지가 있습니다.
| 방식 | 구현 주체 | 특징 |
|---|---|---|
| 직접 구현 (Spin Lock) | 개발자(Redis 명령어 조합) | Redis의 원자적 명령 (SET key value NX PX)을 사용하여 락 획득. 락 실패 시 재시도 전략(Spin Lock 등)과 만료 시간 설정을 직접 구현. 가볍고 세밀한 제어 가능. |
| Redisson 라이브러리 | Redisson 내부 구현 | 고수준 API 제공. 자동 만료 시간 연장, 재진입 락, Redlock 등 복잡한 분산락 안정성 메커니즘을 제공. |
--
1. 직접 구현 (Spin Lock) 방식
RedisTemplate을 이용하면 Redis 기반의 스핀락을 간단히 구현할 수 있습니다.
스핀락은 락을 얻을 때까지 짧은 간격으로 반복 시도(Spin) 하는 방식으로,
락 점유 시간이 짧을 때는 빠르고 효율적이지만, 점유 시간이 길면 요청을 많이 하기 때문에 Redis 부하가 커질 수 있습니다.
예제 코드
Redis Lock을 구현한 코드
@Component
public class RedisLockRepository {
private RedisTemplate<String, String> redisTemplate;
public RedisLockRepository(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
// 무한정 대기하지 않도록 2초이상 걸리면 false로 리턴
public Boolean lock(String key) throws InterruptedException {
long endTime = System.currentTimeMillis()+ 2000;
while(endTime > System.currentTimeMillis()){
Boolean isLocked= redisTemplate
.opsForValue()
.setIfAbsent(key,"lock", Duration.ofSeconds(10));
if(Boolean.TRUE.equals(isLocked)){
return true;
} else {
Thread.sleep(20);
}
}
return false;
}
public Boolean unlock(String key){
return redisTemplate.delete(key);
}
}
클라이언트에서 호출하는 코드
Boolean isLocked = redisLockRepository.lock(key);
if(Boolean.TRUE.equals(isLocked)){
//수행할 로직
redisLockRepository.unlock(key);
}동작 방식 설명
- setIfAbsent(key, value, ttl)
Redis 명령: SET key value NX PX ttl
락이 없을 경우 true 반환, 존재하면 false - 즉시 종료하지 않고 20ms 대기 후 재시도(Spin)
이 과정을 락이 풀릴 때까지 반복 - 사용 후 Lock 해제
해당 방식에도 문제점이 있습니다.
현재 구현은 value가 단순히 "lock"으로 고정되어 있으며, TTL이 10초로 설정되어 있습니다.
이런 경우, TTL이 만료된 후 다른 서버가 같은 key로 다시 락을 획득하더라도,
Redis 입장에서는 이전 락의 소유자와 새로운 락의 소유자를 구분할 수 없습니다.
TTL(10초)이 지나면 Redis는 key를 자동으로 삭제하므로,
다른 서버가 동일한 key로 다시 락을 획득할 수 있습니다.
이 시점에서 Redis는 단순히 key가 존재하지 않는 상태이기 때문에,
락이 해제된 것처럼 보이지만 실제로는 TTL 만료로 인해 key가 삭제된 것뿐입니다.
따라서 락을 생성할 때는 단순 문자열 대신 UUID와 같은 고유 식별자를 value로 저장해야 합니다.
락을 해제할 때는 Redis에 저장된 value(UUID)와 현재 프로세스의 UUID가 일치할 때만 삭제해야 하며,
이를 통해 다른 인스턴스의 락을 잘못 해제하는 문제를 방지할 수 있습니다.
이 검증 과정은 락의 소유자 확인(get)과 삭제(del)를 하나의 원자적(atomic) 연산으로 처리해야 하므로,
Redis의 Lua Script를 사용해 구현해야 합니다.
Lua를 사용하면 이 두 명령을 한 번에 실행해, 락 해제 시 경쟁 조건(race condition)을 방지할 수 있습니다.
본 글에서는 Redis 기반 분산락의 동작 원리를 중점적으로 다루므로,
Lua Script를 이용한 원자적 해제 구현은 다음 포스팅에서 자세히 설명하겠습니다.
2. Redisson 라이브러리 방식
앞서 RedisTemplate을 이용해 직접 락을 구현하면,
TTL, 소유자 검증, 경쟁 조건 처리 등을 직접 관리해야 하는 불편함이 있습니다.
Redisson은 이러한 문제를 자동으로 해결해주는 고수준(High-level) Redis 클라이언트입니다.
락의 획득, 유지, 해제 과정을 모두 안정적으로 관리하며, 특히 Pub/Sub 기반 이벤트를 통해
락 해제 시 다른 스레드 또는 인스턴스에게 즉시 알림을 보내는 구조로 되어 있습니다.
예제 코드
Redisson을 구현한 코드
@Component
public class RedissonLockRepository {
private RedissonClient redissonClient;
public RedissonLockRepository(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}
public Boolean lock(String key){
RLock lock = redissonClient.getLock(key);
try {
// 락 획득을 시도한다(10초동안 시도를 할 예정이며 획득할 경우 1초안에 해제할 예정이다)
Boolean isLocked = lock.tryLock(10,1, TimeUnit.SECONDS);
return isLocked;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public Boolean unlock(String key){
RLock lock = redissonClient.getLock(key);
if(lock.isLocked()){
lock.unlock();
}
return true;
}
}
클라이언트 호출부
redissonLockRepository.lock(key);
// 수행할 로직
redissonLockRepository.unlock(key);
Redisson 내부 동작 구조
Redisson의 RLock은 다음과 같은 단계로 동작합니다.
- SET lockKey value NX PX leaseTime 으로 락 획득 시도
- 실패 시 Redis의 Pub/Sub 채널을 구독 상태로 전환
- 다른 스레드가 락을 해제하면 PUBLISH로 알림 전송
- 대기 중인 스레드는 Pub/Sub 메시지를 수신하고 재시도
- 락을 획득한 스레드는 주기적으로 PEXPIRE로 TTL 자동 연장 (watchdog)
Redisoon 특징
Lua 스크립트 기반 원자성 보장
락 획득(SET NX PX)과 TTL 설정이 하나의 명령으로 실행됨
락 해제 시에는 Lua 스크립트로 value(UUID)를 검증한 뒤 삭제Pub/Sub 이벤트 기반 대기
기존 스핀락 방식과 달리 polling하지 않습니다.
Redis의 PUBLISH/SUBSCRIBE를 활용하여 락이 해제되면 즉시 통보받습니다.
즉, CPU나 Redis 부하가 거의 없습니다.Watchdog(자동 연장) 메커니즘
락을 획득한 스레드가 작업을 수행 중이면, Redisson이 내부적으로 leaseTime을 자동 갱신합니다.
따라서 긴 작업 중에도 TTL 만료로 인한 락 조기 해제 문제가 발생하지 않습니다.
직접 구현 (Spin Lock) Redisson 라이브러리 비교표
| 구분 | 직접 구현 (Spin Lock) | Redisson 라이브러리 |
|---|---|---|
| 락 획득 방식 | setIfAbsent() 반복 (Spin Lock) |
tryLock() (Pub/Sub 이벤트 기반) |
| Deadlock 방지 | TTL 수동 지정 | 자동 watchdog 연장 |
| 락 해제 | 수동 delete 또는 Lua | 내부 Lua 스크립트로 자동 처리 |
| 락 대기 방식 | polling (Thread.sleep) | Pub/Sub 알림 |
| 안정성 | 개발자 관리 | Redisson 자동 관리 |
| 코드 복잡도 | 높음 | 낮음 |
마무리하면서
분산 환경에서 결제, 재고 관리와 같이 동시성 제어가 중요한 비즈니스 로직에는 분산락이 필수적입니다.
특히 여러 서버나 스레드가 동시에 동일한 자원에 접근하는 상황에서는
락을 적절히 제어하지 않으면 중복 처리나 데이터 불일치가 발생할 수 있습니다.
물론 트래픽이 적거나 단일 서버 환경이라면,
추가 인프라 없이 DB Lock(행 단위 락, Pessimistic Lock) 을 사용하는 것도 충분히 유효합니다.
DB 트랜잭션 안에서 락이 관리되므로 별도 구성 없이도 안정적이지만,
확장성이 제한되고 분산 환경으로 확장할 때 병목이 발생할 수 있습니다.
반면 Redis를 이용한 락은 메모리 기반이라 훨씬 빠르고,
멀티 서버 환경에서도 일관성을 보장할 수 있다는 장점이 있습니다.
다만, Redis 인프라를 새로 구축해야 하거나
운영 중인 시스템에서 Redis를 사용하지 않는 경우라면
리소스 관리 측면에서 신중한 접근이 필요합니다.
그럼에도 Redis는 락 외에도 캐시(Cache), 세션 스토어(Session Store), 메시지 큐(Pub/Sub) 등
다양한 활용이 가능하기 때문에, 도입 시 얻을 수 있는 이점이 상당히 큽니다.
그리고 직접 RedisTemplate으로 락을 구현하는 것보다 Redisson을 활용해 구현하는 것이 훨씬 효율적입니다.
Redisson은 TTL, 자동 연장(Watchdog), 소유자 검증, Pub/Sub 기반 락 해제 등
분산락의 핵심 기능을 모두 안전하게 처리하므로, 직접 구현 대비 안정성·생산성·성능 면에서 압도적으로 우수합니다.
'Redis' 카테고리의 다른 글
| Redis 데이터 만료 전략 개념 및 예제 (Feat: TTL, TTI) (0) | 2025.10.12 |
|---|---|
| Spring RedisTemplate 자료구조별 사용법과 CLI 정리 (0) | 2025.09.29 |