들어가며
항상 프로젝트에서는 Redis를 두 가지 목적으로 사용하게 되었다. 첫 번째는 JWT Refresh Token 저장이다. Refresh Token은 만료 시간이 존재하고, 빠르게 조회되어야 하며, 로그아웃이나 재발급 시 삭제 또는 갱신될 수 있다. 이런 특성 때문에 Redis와 잘 어울린다. 이 경우에는 사실 Spring Data Redis만으로도 충분하다.
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
RedisTemplate 또는 StringRedisTemplate을 사용하면 Refresh Token 저장, 조회, 삭제 같은 기본적인 Redis 작업은 충분히 처리할 수 있다.
하지만 두 번째 요구사항에서 고민이 생겼다. 바로 상품 재고 정합성 보장이다. 상품 주문이 동시에 여러 번 들어오는 상황에서는 재고 차감 로직이 동시에 실행될 수 있다. 이때 단순히 DB에서 재고를 조회한 뒤 차감하는 방식으로 구현하면 동시성 문제가 발생할 수 있다. 예를 들어 재고가 1개 남은 상품에 대해 두 명의 사용자가 동시에 주문하면, 두 요청이 모두 "재고가 있다"고 판단한 뒤 각각 주문을 성공시킬 수 있다. 이 문제를 해결하려면 하나의 상품 재고를 차감하는 로직이 동시에 여러 번 실행되지 않도록 제어해야 한다.
즉, 분산 락이 필요해진다. 처음에는 이런 의문이 들었다.
"이미 Spring Data Redis를 사용하고 있는데, 이것만으로 분산 락을 구현할 수는 없을까?"
검색해보면 대부분의 예제는 Redisson을 사용한다.
implementation 'org.redisson:redisson:3.39.0'
그렇다면 여기서 정리해야 할 질문은 단순히 "Redisson을 어떻게 쓰는가?"가 아니다. 정말 중요한 질문은 다음과 같다.
- Spring Data Redis만으로도 분산 락을 구현할 수 있는가?
- 구현할 수 있다면 왜 많은 예제에서는 Redisson을 추천하는가?
- Spring Data Redis 기반 락과 Redisson 기반 락은 어떤 차이가 있는가?
- 상품 재고 정합성을 보장하려면 어떤 방식을 선택하는 것이 적절한가?
이번 글에서는 Redis를 단순 캐시 저장소로 사용하는 관점이 아니라, 상품 재고 정합성을 보장하기 위한 분산 락 관점에서 Spring Data Redis와 Redisson을 비교해보려고 한다. 먼저 두 라이브러리가 각각 무엇인지 정리하고, 이후 실제 예제 코드를 통해 Spring Data Redis로 직접 분산 락을 구현하는 방식과 Redisson을 사용하는 방식을 비교해본다. 최종적으로는 왜 실무에서 Redisson을 많이 사용하는지, 그리고 어떤 상황에서는 Spring Data Redis만으로도 충분한지 판단 기준을 정리해보려고 한다.
근데 왜 RDBMS가 아니라 Redis일까?
본론으로 들어가기 전에 한 가지 의문이 들 수 있다.
"MySQL 같은 RDBMS도 락(Lock) 기능을 지원하는데, 왜 굳이 Redis까지 띄워서 분산 락을 구현할까?"
RDBMS도 `SELECT ... FOR UPDATE` 같은 비관적 락(Pessimistic Lock)이나 `@Version`을 이용한 낙관적 락(Optimistic Lock)을 지원한다. 그럼에도 동시성 해결을 위해 Redis를 선택하는 이유는 크게 성능(속도)과 아키텍처적 관심사 분리 때문이다.
이유는 명확하다. 디스크(Disk) 기반의 RDBMS에서 동시성 락을 잡는 것은 비용이 너무 크기 때문이다. > 선착순 이벤트처럼 수만 명의 요청이 동시에 몰릴 때 RDBMS에 직접 락을 걸면, DB 커넥션이 순식간에 고갈되어 서비스 전체가 다운되는 대참사가 날 수 있다.
반면, 메모리(RAM) 기반의 Redis는 초당 수십만 건의 연산을 처리할 만큼 압도적으로 빠르다. 치열한 '락 획득 경쟁'은 가볍고 빠른 Redis에서 처리하게 두고, 경쟁에서 이긴 단 하나의 요청만 RDBMS에 접근하게 함으로써 메인 데이터베이스를 안전하게 보호할 수 있는 것이다. 즉, 효율적인 '관심사의 분리'와 '성능 확보'를 위해 우리는 Redis를 선택하게 된다. Redis 선택의 이점을 보기 좋게 한 번 정리를 살펴보자.
1. 압도적인 속도 차이 (In-Memory vs Disk)
- RDBMS: 데이터를 디스크(Disk)에 저장하고 관리한다. 디스크 I/O는 태생적으로 느리다. 수많은 사용자가 동시에 재고를 바꾸려고 락을 걸면, DB 커넥션 풀이 마르고 전체 웹 서버가 먹통이 되는 '병목 현상'이 발생한다.
- Redis: 모든 데이터를 메모리(RAM)에 올리고 동작하는 In-Memory DB이다. 연산 속도가 RDBMS와 비교가 안 될 정도로 빠르며(초당 수십만 건), 싱글 스레드로 동작하기 때문에 애초에 락을 획득하고 해제하는 과정 자체가 엄청나게 가볍고 빠르게 처리된다.
2. 비용이 비싼 DB 커넥션 보호
- RDBMS에서 락을 잡고 대기(Wait)하는 시간이 길어지면, 그동안 다른 일반적인 요청(회원 조회, 게시판 조회 등)을 처리해야 할 DB 커넥션까지 모두 묶여버린다.
- 반면 Redis를 분산 락 전용 저장소로 사용하면, "락을 획득하는 치열한 싸움"은 저렴하고 빠른 Redis에서 처리하고, 실제 락을 획득한 딱 1개의 요청만 RDBMS에 접근하게 하므로 메인 데이터베이스를 안전하게 보호할 수 있다.
3. 마이크로서비스(MSA) 및 분산 환경의 제약
- 만약 상품 서비스와 주문 서비스가 고유의 자체 DB를 각각 가지고 있는 분산 환경(MSA)이라면, 하나의 RDBMS 락만으로는 여러 서비스 간의 동시성을 제어할 수 없다.
- 이때 여러 서버와 서비스가 공통으로 바라볼 수 있는 외부 공유 저장소가 필요한데, 그 레이어로 가장 대중적이고 신뢰할 수 있는 기술이 바로 Redis이다.
자! 그럼 본격적으로 이 Redis를 활용해 분산 락을 거는 두 가지 방법, Spring Data Redis와 Redisson을 비교해보자.
1. Spring Data Redis

Spring Data Redis는 Spring 생태계에서 Redis를 사용하기 위한 공식 데이터 접근 모듈이다. Spring Data JPA가 관계형 데이터베이스를 추상화하듯이, Spring Data Redis는 Redis에 대한 접근을 Spring 방식으로 추상화한다. 개발자는 Redis 서버와 직접 통신하는 대신 추상화된 API를 통해 Redis를 사용할 수 있다.
1.1 주요 기능
- RedisTemplate 및 StringRedisTemplate 제공
- Redis Repository 지원
- Spring Cache 연동 및 Pub/Sub 지원
- Transaction 지원 및 Serializer 지원
- Redis Connection 관리
대표적인 사용 예시
- Access Token, Refresh Token 저장
- 이메일 인증 코드 저장
- 캐시 저장 (조회수 캐싱, 인기 게시글 캐싱 등)
// StringRedisTemplate 활용 예시
stringRedisTemplate.opsForValue()
.set("refreshToken:1", token, Duration.ofDays(14));
실무에서 Redis를 단순 Key-Value 저장소로 활용하는 대부분의 경우 Spring Data Redis만으로 충분하다.
2. Redisson
Redisson은 Redis를 기반으로 다양한 분산 객체와 분산 동기화 기능을 제공하는 Java Redis Client이다. Spring Data Redis가 "데이터 저장"에 초점을 맞춘다면, Redisson은 "분산 환경에서의 동시성 제어"에 초점을 맞춘 라이브러리라고 볼 수 있다. Redis를 단순 캐시 서버가 아닌 하나의 분산 시스템 인프라로 활용할 수 있게 해준다.
2.1 주요 기능
- Distributed Lock (Fair Lock, Read/Write Lock, Semaphore 등)
- Distributed Queue / Delayed Queue
- Distributed Map / Set
- AtomicLong 및 Distributed Executor Service
대표적인 사용 예시
- 선착순 쿠폰 발급 및 재고 차감
- 중복 결제 및 중복 주문 방지
- 배치 및 스케줄러 중복 실행 방지
// Redisson 분산 락 활용 예시
RLock lock = redissonClient.getLock("product:1");
lock.lock();
try {
decreaseStock();
} finally {
lock.unlock();
}
왜 Spring 공식 라이브러리는 분산 락을 제공하지 않을까?
많은 개발자가 처음 Redis를 접하면 이런 의문을 가진다. "Spring Data Redis가 있는데 왜 Redisson이 따로 존재할까?" 그 이유는 Spring Data Redis의 목적이 다르기 때문이다. Spring Data Redis는 이름 그대로 "Data Access Layer(데이터 접근 계층)"를 담당한다. 즉, Redis 연결, 데이터 저장/조회, 캐시 처리에 집중한다. 반면 분산 락은 단순 데이터 저장 문제가 아니다. 분산 락을 안전하게 구현하려면 다음과 같은 복잡한 문제를 해결해야 한다.
- 락 획득 경쟁 (스핀 락으로 인한 Redis 부하 방지)
- 락 만료 처리 및 데드락(Dead Lock) 방지
- 네트워크 장애 및 프로세스 비정상 종료 대응
- Redis Cluster 환경 대응
Spring 팀은 이러한 영역을 Redis 접근 계층의 책임 범위를 넘어서는 문제로 판단하고 있다. 그래서 Spring Data Redis는 기본적인 Redis 접근 기능만 제공하고, 분산 락 같은 고급 기능은 Redisson 같은 전문 라이브러리에 맡기고 있다.
Spring Data Redis와 Redisson 한눈에 비교
| 항목 | Spring Data Redis | Redisson |
| 목적 | Redis 데이터 접근 (CRUD) | 분산 시스템 및 동시성 제어 지원 |
| 주요 객체 | RedisTemplate, StringRedisTemplate | RedissonClient, RLock |
| 사용 난이도 | 낮음 | 중간 |
| 캐시 / KV 저장 | O | O |
| 분산 락 지원 | X (직접 로직을 구현해야 함) | O (자체 내장) |
| 분산 자료구조 | X | O (RMap, RList 등) |
| Spring Boot 자동설정 | O (기본 제공) | 부분적 (별도 스타터 추가 필요) |
| 실무 사용 빈도 | 매우 높음 | 높음 |
각각 언제 선택해야 할까?
- Spring Data Redis만 사용하면 되는 경우: Refresh/Access Token 저장, 인증번호 저장, 단순 캐시 서버 구축, 세션 저장 등. 이 경우에는 RedisTemplate만으로 충분하다.
- Redisson이 필요한 경우: 쿠폰 선착순 이벤트, 재고 관리, 결제 처리, 예약 시스템 등 동시성 문제 해결이 핵심일 때. 특히 여러 서버가 동시에 동작하는 분산 환경에서는 사실상 Redisson 같은 분산 락 솔루션이 필요해진다.
두 라이브러리의 장단점
Spring Data Redis
- 장점: Spring Boot와 뛰어난 통합성, 간단한 설정, 공식 지원으로 인한 낮은 러닝 커브.
- 단점: 분산 락 기능 부재, 동시성 제어 기능 부족, 고급 분산 자료구조 없음.
Redisson
- 장점: 강력한 분산 락 지원, 다양한 분산 객체 제공, Redis Cluster 완벽 지원, 그리고 Watch Dog 기능 제공.
- 단점: 추가 학습 필요, 설정이 상대적으로 복잡함, 잘못 사용하면 성능 이슈 발생 가능.
Redisson의 강력한 무기: Watch Dog 기능
일반적인 Redis Lock은 타임아웃(TTL)이 만료되면 락이 자동 해제된다. 그런데 서버에서 작업이 끝나지 않았는데 TTL이 먼저 만료되면 다른 서버가 동일한 작업을 수행하는 동시성 문제가 터질 수 있다. Redisson은 Watch Dog를 통해 락을 보유한 스레드가 살아있는 동안 TTL을 자동으로 연장해 준다. 덕분에 개발자가 락 만료 시간을 일일이 계산할 필요가 없다. 실무에서 Redisson을 쓰는 가장 큰 이유 중 하나다.
결론: 현재 프로젝트에서는 왜 둘 다 사용할까?
현재 프로젝트는 JWT Refresh Token을 Redis에 저장하기 위해 처음 Redis를 도입했다. 이 경우 실제 토큰의 가벼운 저장 및 만료(TTL) 처리는 Spring Data Redis(StringRedisTemplate)가 담당한다. 반면, 향후 추가될 수 있는 중복 결제 방지, 재고 관리, 선착순 쿠폰 발급 등의 동시성 문제를 안전하게 해결하기 위해 Redisson을 함께 구축해 두었다. 즉, 현재 우리 프로젝트의 구조는 다음과 같이 명확한 역할 분리가 이루어진 상태라고 볼 수 있다.
- Spring Data Redis: 일반 데이터 저장 및 토큰 관리 담당
- Redisson: 안전한 분산 락 및 동시성 제어 담당
2.2 Redisson 도입 시 가장 많이 하는 고민: 어떤 의존성을 선택해야 할까?
막상 프로젝트에 Redisson을 도입하려고 공식 문서나 여러 기술 블로그를 찾아보면, 비슷비슷한 이름의 의존성이 여러 개 등장하여 우리를 혼란스럽게 만든다.
implementation 'org.redisson:redisson:{버전}'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-session-data-redis'
처음에는 모두 비슷해 보이지만 각각의 목적이 조금씩 다르다.
따라서 프로젝트 구조와 내가 필요한 사용 목적에 따라 적절한 의존성을 선택해야 한다.
① org.redisson:redisson (순수 코어)
가장 기본이 되는 Redisson의 핵심 라이브러리다. 스프링 프레임워크와 무관하게 사용할 수 있는 순수 Java Redis Client이며, Redisson이 제공하는 모든 기능이 이 안에 포함되어 있다.
implementation 'org.redisson:redisson:3.39.0'
- 특징: 이 의존성만 추가하면 분산 락, 분산 컬렉션, 세마포어(Semaphore) 등의 고급 기능을 바로 사용할 수 있다. 다만, Spring Boot의 자동 설정(Auto Configuration) 기능은 제공하지 않으므로 개발자가 직접 자바 코드로 RedissonClient를 Bean으로 등록해 주어야 한다.
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://localhost:6379");
return Redisson.create(config);
}
}
- 장점: * Spring Boot 버전에 영향을 거의 받지 않아 호환성 리스크가 없다.
- 설정이 눈에 보이기 때문에 명확하다.
- 내가 필요한 기능(분산 락 등)만 딱 깔끔하게 독립적으로 가져다 쓰기 좋다.
- 단점: * 직접 Java Config 클래스를 작성해 Bean 설정을 해야 하므로 번거로울 수 있다.
- 스프링 부트가 제공하는 자동 설정의 편의성을 누릴 수 없다.
② org.redisson:redisson-spring-data-XX (스프링 데이터 연동)
Spring Data Redis와 Redisson을 깊게 통합하기 위한 연동 모듈이다.
여기서 뒤에 붙는 숫자는 프로젝트의 Spring Data Redis 버전을 의미한다.
implementation 'org.redisson:redisson-spring-data-34'
- 특징: 이 모듈은 일반적인 분산 락 기능만을 쓰기 위해 단독으로 추가하는 경우는 거의 없다. 주로 RedisConnectionFactory 수준에서 기본 클라이언트(Lettuce 등)를 배제하고, Redisson을 Spring Data Redis의 기본 엔진으로 통합하여 연동하려는 경우에 사용한다. 실무에서 일반적인 백엔드 개발자가 직접 이 모듈만 단독으로 다룰 일은 많지 않다.
③ org.redisson:redisson-spring-boot-starter (스프링 부트 스타터)
스프링 부트의 강력한 무기인 '자동 설정'을 지원하는 스타터 의존성이다.
implementation 'org.redisson:redisson-spring-boot-starter'
- 특징: 의존성을 추가한 후, 별도의 Java Config 클래스 없이 application.yml에 설정만 적어주면 스프링 부트가 자동으로 RedissonClient Bean을 띄워준다.
spring:
data:
redis:
host: localhost
port: 6379
- 장점: * 설정 파일 작성만으로 끝나므로 도입이 매우 간단하고 빠르다.
- Spring Boot 생태계와 자연스럽게 통합된다.
- 단점: * 내부적으로 특정 버젼의 spring-boot-starter-data-redis를 포함하고 있어, 현재 내가 사용하는 Spring Boot 버전 및 Spring Data Redis 버전 조합과 호환되는지 매번 확인해야 한다.
- 버전 조합이 어긋날 경우 의존성 충돌(MethodNotFoundException 등)이 발생할 위험이 있다.
락 전용 유틸리티로서의 명확한 분리
현재 우리 프로젝트는 다음과 같이 의존성을 구성했다.
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.redisson:redisson:3.39.0'
우리 프로젝트의 목적은 아주 명확했다.
- Refresh Token 저장 및 조회
- 일반적인 Redis 캐시 사용
- 상품 재고 차감 시 동시성을 제어할 분산 락
핵심은, 일반적인 Redis 데이터 접근은 기존의 Spring Data Redis(Lettuce)가 전담하고, 동시성 제어가 필요한 분산 락 기능만 Redisson이 부분적으로 담당하면 충분한 구조였다. 기존에 잘 돌아가고 있던 RedisTemplate이나 StringRedisTemplate의 내부 구조를 Redisson 기반으로 통째로 교체하거나 깊게 통합할 이유가 전혀 없었던 것이다. 따라서 버전 충돌 리스크가 있는 자동 설정 스타터 대신, 가장 심플하고 독립적인 순수 Redisson 코어 라이브러리(org.redisson:redisson)를 선택했고, 분산 락에 필요한 RedissonClient만 명시적으로 Bean 등록하여 사용하기로 했다.
결론: 어떤 의존성을 선택해야 할까?
언제나 그렇듯 기술에 정답은 없다. 내 프로젝트의 아키텍처와 도입 목적에 따라 아래의 기준을 참고하여 선택하면 된다.
| 내가 직면한 상황 | 추천하는 의존성 |
| Spring Data Redis(Lettuce)는 유지하고, 분산 락 유틸만 추가하고 싶을 때 | `org.redisson:redisson` (순수 코어) |
| 코딩 없이 yml 설정만으로 빠르게 Redisson 자동 설정을 쓰고 싶을 때 | `org.redisson:redisson-spring-boot-starter` |
| Spring Data Redis 내부 엔진(ConnectionFactory)까지 Redisson으로 통합할 때 | `org.redisson:redisson-spring-data-XX` |
현재 프로젝트처럼 Spring Data Redis의 기본 뼈대와 Refresh Token 저장 로직은 그대로 유지하면서, 상품 재고 정합성을 위한 분산 락 기능만 깔끔하게 얹고 싶다면 가장 단순하고 안전한 구조는 아래의 조합이다.
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.redisson:redisson:3.39.0'
이 구조를 통해 우리는 각 라이브러리의 장점만을 취하며, "토큰 저장은 Spring Data Redis, 분산 락은 Redisson"이라는 명확한 역할 분리를 이뤄낼 수 있다.
3. Spring Data Redis로 분산 락 직접 구현하기
지금까지 Spring Data Redis와 Redisson의 역할 차이를 살펴봤다. 그런데 아직 가장 중요한 질문이 남아 있다.
"Spring Data Redis만으로도 분산 락을 구현할 수 있는데?"
맞다. Redisson이 내부적으로 사용하는 핵심 아이디어도 결국 Redis 명령어를 활용한 분산 락이다. 따라서 Redis가 제공하는 기능을 이용하면 외부 라이브러리 없이 Spring Data Redis만으로도 충분히 분산 락을 구현할 수 있다. 하지만, 이를 실무 레벨에서 '안전하게' 직접 구현하는 과정에서는 생각보다 많은 문제에 직면하게 된다.
분산 락의 목표는 무엇일까?
분산 락은 어렵게 생각할 필요가 없다. 목표는 단 하나다. "특정 로직을 동시에 하나의 요청만 수행하도록 보장하는 것". 예를 들어 재고가 1개 남은 상품이 있다고 가정해 보자.
- 재고: 1개
- 사용자 A 주문 요청
- 사용자 B 주문 요청
두 요청이 동시에 들어온다고 가정해보자.
Product product = productRepository.findById(id);
if (product.getStock() > 0) {
product.decreaseStock();
}
두 요청 모두 "재고가 1개 남아있네?" 라고 판단할 수 있다.
결과적으로,
- A 주문 성공
- B 주문 성공
이 되어 실제 1개의 재고보다 더 많은 주문이 발생할 수 있다. 결과적으로 A와 B의 주문이 모두 성공 처리되어, 실제 재고인 1개보다 더 많은 주문이 발생하는 데이터 정합성 오류가 터진다. 이 문제를 해결하기 위해 "상품 재고 차감 로직에는 동시에 하나의 요청만 진입"하도록 문을 잠가야(Lock) 한다.
Redis는 어떻게 락을 만들까?
Redis에는 락 구현에 아주 적합한 원자적(Atomic) 명령어가 존재한다.
SET lock:product:1 uuid NX EX 3
각 옵션의 의미는 다음과 같다.
- `NX`: Key가 존재하지 않을 때만 저장 (Set if Not Exists)
- `EX 3`: 3초 뒤에 Key를 자동 만료 제거 (Expire)
SET lock:product:1 uuid NX EX 3
따라서 명령이 성공하면 "락 획득 성공"이고, 실패하면 "이미 누군가 락을 보유 중" 이라는 의미가 된다.
3.1 Spring Data Redis에서는 어떻게 구현
Spring Data Redis에서는 다음과 같이 구현할 수 있다.
Boolean acquired =
stringRedisTemplate
.opsForValue()
.setIfAbsent(lockKey,lockValue,Duration.ofSeconds(3));
내부적으로는 다음 Redis 명령어가 실행된다.
SET lock:product:1 uuid NX EX 3
if (Boolean.TRUE.equals(acquired)) {
// 락 획득 성공
}
이면 해당 요청만 재고 차감 로직에 진입할 수 있다.
실제 재고 차감 예제
String lockKey = "lock:product:" + productId;
String lockValue = UUID.randomUUID().toString();
Boolean acquired =
stringRedisTemplate
.opsForValue()
.setIfAbsent(lockKey,lockValue,Duration.ofSeconds(3));
if (!Boolean.TRUE.equals(acquired)) {
throw new IllegalStateException("이미 처리 중입니다.");
}
try {
Product product = productRepository.findById(productId).orElseThrow();
product.decrease(quantity);
} finally {
stringRedisTemplate.delete(lockKey);
}
여기까지만 보면 꽤 괜찮아 보인다. 실제로 분산 락도 동작한다. 그래서 처음에는 이런 생각이 든다.
"어? Redisson 필요 없는 거 아닌가?" 하지만 실무 환경에서는 바로 여기서부터 진짜 지옥 같은 예외 상황들이 시작된다.
3.2 문제 1. 내가 건 락만 해제해야 한다 (소유권 검증 문제)
현재 코드의 `finally` 블록에서는 락을 해제할 때 단순히 Key를 삭제하고 있다.
stringRedisTemplate.delete(lockKey);
그런데 이런 상황을 생각해 보자.
- 사용자 A가 락을 획득하고 작업을 시작함
- 생각보다 대량의 연산이 걸려 A의 작업이 지연됨 → 그 사이에 설정한 TTL(3초)이 만료되어 락이 자동 해제됨
- 락이 풀리자마자 대기하던 사용자 B가 새롭게 락을 획득하고 작업을 시작함
- 뒤늦게 사용자 A의 작업이 끝나고 finally 블록의 delete(lockKey)를 호출함
결과는? 사용자 A가 사용자 B가 보유한 락을 강제로 삭제해 버린다. 락이 강제로 풀린 상태에서 사용자 C, D가 연이어 진입하면 동시성 지옥이 펼쳐진다. 따라서 락을 해제할 때는 반드시 `[현재 Redis에 저장된 값 == 내가 처음 저장했던 lockValue(UUID)]` 인지 확인하는 소유권 검증 단계가 필수적이다. 단순 delete로는 해결할 수 없다.
3.3 문제 2. 락 유지 시간(TTL)의 딜레마
현재 예제에서는 Duration.ofSeconds(3)을 사용했다. 그렇다면 적절한 시간은 과연 얼마일까? 1초? 5초? 30초? 정답이 없다.
- TTL을 너무 길게 잡으면: 락을 쥔 서버가 작업 도중 다운되었을 때, 락이 해제되지 않아 다른 서버들이 오랜 시간 동안 데드락(Dead Lock)에 걸린다.
- TTL을 너무 짧게 잡으면: 비즈니스 로직이 실행되는 도중에 락이 먼저 풀려버려 분산 락 자체가 무력화된다.
3.4 문제 3. 작업 시간이 TTL보다 길어질 때의 대안 부재
문제 2의 연장선으로, 만약 DB 가동률 증가나 외부 API 호출 지연으로 인해 실제 비즈니스 로직 처리 시간이 내가 설정한 TTL을 넘어서는 순간, 분산 락 시스템은 완전히 붕괴한다. 내가 아무리 예측을 잘해서 TTL을 설정하더라도, 인프라 상황에 따른 '작업 시간의 변동성'을 코드 한 줄로 방어하기란 불가능에 가깝다.
3.5 문제 4. 락 획득 실패 시 '재시도'로 인한 Redis 과부하 (Spin Lock)
현재 코드는 락 획득에 실패하면 즉시 예외를 던진다. 하지만 실무 선착순 이벤트라면 사용자는 대기했다가 다시 시도할 수 있어야 한다. 스프링 데이터 레디스로 재시도를 구현하려면 while 루프를 돌며 일정 시간 간격(Thread.sleep)으로 계속 락 획득을 요청하는 스핀 락(Spin Lock) 구조를 짜야 한다. 이 방식은 락이 풀릴 때까지 수많은 요청이 Redis 서버에 무차별적으로 setIfAbsent() 공격을 퍼붓기 때문에 Redis의 CPU 사용량을 폭발시키는 주범이 된다.
3.6 문제 5. 락 검증과 삭제의 원자성(Atomic) 보장 문제
문제 1을 해결하기 위해 락을 해제할 때 검증 로직을 넣는다고 해보자.
// [의도한 로직] 내가 건 락인지 확인하고 삭제하자!
if (lockValue.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
stringRedisTemplate.delete(lockKey); // 찰나의 순간에 문제가 터진다면?
}
이 로직은 안전해 보이지만, '값 조회'와 '삭제'라는 두 개의 명령어로 나뉘어 있다. 만약 조회(get)를 성공적으로 마치고 삭제(delete) 명령을 내리기 직전, 그 찰나의 순간(0.001초)에 락의 TTL이 만료되어 다른 서버가 락을 채갔다면 어떻게 될까? 검증은 통과했으므로 결국 다른 서버의 락을 지워버리는 대참사가 똑같이 발생한다. 결국 이 검증과 삭제는 단 하나의 명령처럼 묶여서 실행되는 원자성(Atomic)이 보장되어야 하며, 이를 위해 스프링 진영에서 다소 생소하고 복잡한 Lua Script까지 직접 작성해서 레디스에 넘겨주어야 한다.
3.7 직접 구현은 가능하지만...
결론적으로 Spring Data Redis만으로도 분산 락은 충분히 구현할 수 있다. 하지만 완성도 높은 분산 락을 만들려면 락 소유자 검증, 스핀 락 해결, 만료 시간 동적 연장, 원자적 해제 처리 등을 개발자가 바닥부터 전부 제어해야 한다. 배보다 배꼽이 더 커지는 셈이다. 바로 이 지점에서 구원투수로 등장하는 것이 Redisson이다. 다음 장에서는 Redisson이 이 복잡한 문제들을 단 몇 줄의 코드로 어떻게 우아하게 해결하는지 살펴보자.
'🍃SpringBoot' 카테고리의 다른 글
| Spring for Redis[3/3]: Redisson Distributed Lock 구현(SETNX) (0) | 2026.06.09 |
|---|---|
| Spring for Redis[2/3]: Redisson 의사결정(Optimistic Lock에서Distributed Lock) (0) | 2026.06.09 |
| JWT[3/3]: JWT 실전 사용(Redis Lua Script, JWT BlackList, Rotation) (0) | 2026.06.07 |
| JWT[2/3]: SpringBoot에서 JWT 인증 구조와 정책(Spring Security) (1) | 2026.06.07 |
| JWT[1/3]: JWT 이해(이론) (0) | 2026.06.06 |
