-
[Spring] Redis의 Redisson을 활용한 분산락 처리Spring 2025. 5. 10. 19:40반응형
개요
동시성 제어를 위한 락 방식은 여러 가지가 있고, 접근 레벨에 따라서도 적용할 락 방식이 다릅니다. 예를 들어, 하나의 프로세스 안에서의 동기화, 여러 프로세스 간의 동기화, 또는 분산 시스템에서의 동기화 등 목적에 따라 요구사항과 복잡성이 달라집니다. 그 중에서도 다중 스레드 환경과 분산 시스템 환경 모두에서 동시성 제어를 보장하기 위해 Redis의 Redisson을 통한 분산락 처리에 대해 알아보겠습니다.
1. 락의 종류와 차이점
동시성 제어를 위해 사용되는 락은 크게 다음과 같이 분류할 수 있습니다.
종류설명
정리락 (Mutex) 단일 프로세스, 단일 스레드 간 동시성 제어 관적락 (Monitor Lock) JVM의 synchronized 키워드로 구현된 락 비관적 락 데이터 접근 시마다 락 걸어 충돌 방지 낙관적 락 충돌 가능성 적다고 가정, 나중에 검증 후 롤백 분산락 여러 서버/프로세스 간 동시성 제어 필요 이 중 분산락은 여러 서버가 동시에 동일 자원에 접근할 수 있는 환경에서 데이터를 보호하기 위해 반드시 필요합니다.
2. Redis를 이용한 분산락과 Redisson의 필요성
Redis는 SETNX, EXPIRE 등의 명령어 조합으로 락 기능을 직접 구현할 수 있습니다. 하지만 락 소유권 상실, 락 해제 실패, 클라이언트 장애로 인한 락 미해제 등의 문제점이 발생할 수 있습니다.
이를 보완하기 위해 Redisson이 등장했습니다. Redisson은 Redis 기반으로 안정적이고 고가용성 락 기능을 제공하는 Java 클라이언트 라이브러리로, 다음 기능을 지원합니다.
✅ 자동 락 연장 (Watchdog)
✅ 페일오버 대응
✅ 다양한 락 유형 (공정 락, 읽기/쓰기 락 등)
✅ java.util.concurrent와 유사한 API
✅ pub/sub 기반의 락 해제 알림: 락 해제 시 Redis Pub/Sub 채널을 통해 대기 중인 클라이언트들에게 "락을 획득해도 된다"는 메시지를 전송하여, 락을 polling 없이 효율적으로 관리.
또한 Redis는 Single Thread 구조로 하나의 자원에 여러 스레드가 동시에 접근하지 않도록 보장하며, 자료구조 자체가 Atomic한 성질을 가지고 있어 Critical Section에 대한 동기화를 제공합니다.
클러스터 환경에서도 Redis의 Atomic한 특성 덕분에 데이터의 일관성을 보장합니다.
3. Redisson vs Lettuce Spin Lock
많은 분들이 Redis 기반 락 구현 시 Lettuce의 Spin Lock과 비교를 합니다.
항목 Redisson Lettuce Spin Lock 락 방식 Blocking Lock (Watchdog + pub/sub) Spin Lock (루프 재시도) 타임아웃 관리 자동 연장 (기본 30초 watchdog) 클라이언트 로직으로 직접 처리 락 해제 방식 pub/sub로 대기자 통보 클라이언트 polling 필요 API lock(), tryLock() 지원 SETNX 직접 사용 성능 안정성 중시 성능 중시 (낮은 latency) 락 해제 안정성 Redisson 내부 관리 직접 구현 필요 Redisson을 선택해야 하는 이유:
✅ pub/sub 기반 락 해제 알림으로 busy waiting 회피
✅ 락 소유권 상실 방지 (Watchdog 자동 연장)
✅ 공정 락, 읽기/쓰기 락, 세마포어 등 고급 동시성 제어 지원
✅ 코드의 단순화와 유지보수성 확보
분산 환경에서 성능보다 안정성과 기능 확장성이 중요한 경우 Redisson 사용을 권장합니다.
4. Spring Boot 3.4, Java 17, Gradle 환경 설정
(1) build.gradle 설정
implementation 'org.redisson:redisson-spring-boot-starter:3.23.5'
(2) application.yml 설정
spring: redis: host: localhost port: 6379 redisson: config: classpath:redisson-config.yaml
resources/redisson-config.yaml 파일 생성:
singleServerConfig: address: "redis://127.0.0.1:6379"
5. Redisson Sample Code
(1) StockService + RedissonLockFacade
package com.example.stock.facade; import com.example.stock.service.StockService; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.stereotype.Component; import java.util.concurrent.TimeUnit; @Component public class RedissonLockStockFacade { private final RedissonClient redissonClient; private final StockService stockService; public RedissonLockStockFacade(RedissonClient redissonClient, StockService stockService) { this.redissonClient = redissonClient; this.stockService = stockService; } public void decrease(Long key, Long quantity) { RLock lock = redissonClient.getLock(key.toString()); try { boolean available = lock.tryLock(5, 1, TimeUnit.SECONDS); if (!available) { System.out.println("Lock 획득 실패"); return; } stockService.decrease(key, quantity); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { lock.unlock(); } } }
✅ redissonClient.getLock(key.toString()): key 기반 락 객체 생성 → 동일 key로 요청 시 동일 락을 공유.
✅ tryLock(5,1, TimeUnit.SECONDS): 최대 5초간 락 시도, 획득 시 1초간 락 유지, 실패 시 종료.
✅ lock.unlock(): 락 해제 → pub/sub로 대기 클라이언트에 알림 전송.
(2) 테스트 코드
package com.example.stock.facade; import com.example.stock.domain.Stock; import com.example.stock.repository.StockRepository; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import static org.junit.jupiter.api.Assertions.assertEquals; @SpringBootTest class RedissonLockStockFacadeTest { @Autowired private RedissonLockStockFacade redissonLockStockFacade; @Autowired private StockRepository stockRepository; @BeforeEach public void before() { Stock stock = new Stock(1L, 200L); stockRepository.save(stock); } @AfterEach public void after() { stockRepository.deleteAll(); } @Test public void 동시에_30개의_요청() throws InterruptedException { int threadCount = 200; ExecutorService executorService = Executors.newFixedThreadPool(30); CountDownLatch countDownLatch = new CountDownLatch(threadCount); for (int i = 0; i < threadCount; i++) { executorService.submit(() -> { try { redissonLockStockFacade.decrease(1L, 1L); } finally { countDownLatch.countDown(); } }); } countDownLatch.await(); Stock stock = stockRepository.findById(1L).orElseThrow(); assertEquals(0L, stock.getQuantity()); } }
✅ 스레드 풀 최대 30개, 스레드 200개 생성, 30개 요청 동시 테스트 → Redisson 기반 안정적으로 락 관리 및 동시성 제어 처리 확인
마무리
Redisson은 Redis 기반 분산락 구현 시 안정성, 편리성, 기능성을 모두 제공하는 솔루션입니다.
특히 pub/sub 기반의 락 해제 알림 메커니즘과 Watchdog 기반의 자동 연장 기능은 분산 환경에서의 락 유지를 더욱 안전하게 만들어줍니다.
Lettuce Spin Lock이 성능에 유리할 수 있지만, CPU 리소스 과다 소모, polling에 의존한 락 획득 방식 등의 단점이 있어 안정성과 유지보수성이 요구되는 실무에서는 Redisson 사용을 권장합니다.
반응형'Spring' 카테고리의 다른 글
[Spring] Lock 종류 정리 (낙관적 락, 비관적 락, 분산락, 데드락, 등) 및 예시 (0) 2025.05.08 [Aws + Spring] SNS(Simple Notification Service) + Spring Boot 에러 메일 전송 기능 구축하기 (0) 2024.01.20 [Aws + Spring] Spring Boot + Aws Glue + S3를 활용한 방문자 통계 구축 (1) (0) 2024.01.02 [Spring] Spring Boot에서 아임포트 다날 본인인증 연동(Rest Api) (0) 2023.09.21 Spring boot Gitlab CI/CD 구축 (2) (0) 2023.09.19