Redis + Redisson 기반 분산락을 활용한 재고 관리 시스템 구현 예제
src/main/kotlin/com/example/demo/
├── config/
│ ├── RedissonConfig.kt # Redisson 클라이언트 설정
│ └── DataLoader.kt # 초기 데이터 로딩
├── lock/
│ └── DistributedLockTemplate.kt # 분산락 템플릿 (핵심)
├── domain/
│ └── Stock.kt # 재고 엔티티
├── repository/
│ └── StockRepository.kt # 재고 리포지토리
├── service/
│ └── StockService.kt # 재고 비즈니스 로직
└── controller/
├── StockController.kt # 재고 관리 API
└── ConcurrencyTestController.kt # 동시성 테스트 API
Docker 사용 (권장)
docker run -d --name redis -p 6379:6379 redis:latest로컬 설치
- Windows: https://github.com/microsoftarchive/redis/releases
- Mac:
brew install redis && redis-server - Linux:
sudo apt-get install redis-server && redis-server
Redis 연결 확인:
redis-cli ping
# PONG 응답이 나와야 함./gradlew clean build./gradlew bootRun서버가 http://localhost:8080 에서 실행됩니다.
브라우저에서 http://localhost:8080/h2-console 접속
- JDBC URL:
jdbc:h2:mem:testdb - Username:
sa - Password: (비워두기)
curl -X POST http://localhost:8080/api/stock \
-H "Content-Type: application/json" \
-d '{"productName": "노트북", "quantity": 100}'curl http://localhost:8080/api/stock/1curl -X POST http://localhost:8080/api/stock/1/decreasecurl -X POST http://localhost:8080/api/stock/1/decrease-no-lockcurl -X POST "http://localhost:8080/api/test/fire-with-lock?stockId=1&threads=100"예상 결과:
{
"testType": "분산락 사용",
"totalThreads": 100,
"successCount": 100,
"failCount": 0,
"finalQuantity": 0,
"durationMs": 2500
}- 재고가 정확히 100 감소
curl -X POST "http://localhost:8080/api/test/fire-without-lock?stockId=1&threads=100"예상 결과:
{
"testType": "락 없음 (Race Condition 발생 가능)",
"totalThreads": 100,
"successCount": 100,
"failCount": 0,
"finalQuantity": 30,
"durationMs": 800
}- Race Condition으로 인해 재고가 예상보다 적게 감소 (30~70 남음)
curl -X POST "http://localhost:8080/api/test/fire-with-retry?stockId=1&threads=100"예상 결과:
{
"testType": "분산락 사용 (재시도 포함)",
"totalThreads": 100,
"successCount": 100,
"failCount": 0,
"finalQuantity": 0,
"durationMs": 3200
}fun <T> lock(
lockKey: String,
waitTime: Long = 5, // 락 획득 대기 시간 (초)
leaseTime: Long = 3, // 락 자동 해제 시간 (초)
block: () -> T
): T {
val lock = redissonClient.getLock(lockKey)
val acquired = lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS)
if (!acquired) {
throw IllegalStateException("Lock 획득 실패: $lockKey")
}
return try {
block()
} finally {
if (lock.isHeldByCurrentThread) {
lock.unlock()
}
}
}fun decreaseWithLock(stockId: Long) {
val lockKey = "stock:lock:$stockId"
lockTemplate.lock(lockKey) {
decreaseStock(stockId)
}
}- 락 획득을 기다리는 최대 시간
- 이 시간 동안 락을 획득하지 못하면 실패
- 기본값: 5초
- 락을 자동으로 해제하는 시간
- Deadlock 방지를 위한 안전장치
- 기본값: 3초
- 작업 시간보다 충분히 길게 설정
Unable to connect to Redis server
해결: Redis 서버가 실행 중인지 확인
docker ps | grep redis
# 또는
redis-cli pingLock 획득 실패: stock:lock:1
원인:
- waitTime이 너무 짧음 → 늘려보기
- leaseTime이 너무 짧아서 작업 완료 전에 락이 해제됨
해결: 파라미터 조정
lockTemplate.lock(lockKey, waitTime = 10, leaseTime = 5) {
// ...
}애플리케이션을 재시작하면 데이터가 자동으로 초기화됩니다.
(ddl-auto: create 설정)
- 재고: 100개
- 동시 요청: 100개
- 스레드 풀: 32
| 구분 | 처리 시간 | 최종 재고 | 정확성 |
|---|---|---|---|
| 분산락 사용 | ~2.5초 | 0 | ✅ 100% |
| 분산락 없음 | ~0.8초 | 30~70 | ❌ 30~70% |
| 재시도 포함 | ~3.2초 | 0 | ✅ 100% |
결론: 분산락 없이는 3배 빠르지만 데이터 정합성이 깨짐
- ✅ 재고 차감
- ✅ 결제 승인
- ✅ 좌석 예약
- ✅ 쿠폰 발급
- ✅ 중복 주문 방지
- ❌ 단순 읽기 작업
- ❌ 긴 배치 작업 (락 타임아웃 위험)
- ❌ 높은 처리량이 필요한 API (성능 저하)
- leaseTime을 반드시 설정
- 작업 시간보다 충분히 길게
- 락 내부에서 Thread.sleep() 금지
- 락 범위를 최소화
- blocking I/O 금지
- 빠른 작업만 락으로 보호
- Fail Fast 전략 권장
- 필요시 Fallback 로직 구현
- 모니터링 필수