안녕하딤니카?
이번에는 저번에 작성했던 MySQL에서 제공하는 Lock 세 가지를 활용하는 방법에 이어서,
Redis의 두 가지 라이브러리(Lettuce, Redisson)를 활용해서 동시성 이슈를 해결하는 방법을 알아보겠습니다.
저번 글은 아래에서 확인할 수 있습니다!
MySQL에서 제공하는 Lock을 이용해서 동시성 제어하기 (Pessimistic Lock, Optimistic Lock, Named Lock)
안녕하딤니카? 또 티스토리 블로그 방치 이슈...오늘은 저번에 슬렁슬렁 공부하던 동시성 제어를 좀 더 공부해 보기 위해서 글을 작성해 봅니다바로~ MySQL에서 제공하는 Lock을 이용해서 동시성
chuuuu1224.tistory.com
그럼 시~작
1. 개요
저번에는 MySQL의 Lock을 활용해서 동시성을 제어하고, Race Condition 및 다양한 문제를 방지하고 동시성을 제어하는 방법에 대해서 알아봤습니다. 이번에는 Redis라는 오픈 소스 인메모리 데이터 구조 저장소를 활용해서 동시성을 제어하는 방법에 대해서 알아보겠습니다.
Redis의 개념과 Redis를 사용하면 좋은 점, 단점, 사용 시 주의사항은 아래의 글에 자세히 적어두었습니다!
선착순 쿠폰 발급 시스템 개발을 통해 Redis 랑 Kafka 찍먹해보기
안녕하세요? 재영입니따 프로젝트 개발을 끝내고 오랜만에 블로그에 왔습니다오늘 정리해볼건 여태 안 듣고 미뤄놨던 Redis 랑 Kafka 찍먹 강의를 듣고학습한 걸 끄적여보려고 합니다 이거 진
chuuuu1224.tistory.com
동시성 제어를 위해 Redis의 Lettuce와 Redisson 라이브러리를 알아보고, 각 라이브러리를 사용하는 방법을 알아보겠습니다.
이 내용은 아래 강의를 수강하고 적는 글입니다!
강의 정보 : 인프런 - 재고시스템으로 알아보는 동시성이슈 해결방법 (최상용)
재고시스템으로 알아보는 동시성이슈 해결방법 강의 | 최상용 - 인프런
최상용 | 동시성 이슈란 무엇인지 알아보고 처리하는 방법들을 학습합니다., 동시성 이슈 처리도 자신있게! 간단한 재고 시스템으로 차근차근 배워보세요. 백엔드 개발자라면 꼭 알아야 할 동
www.inflearn.com
2. Redis의 Lettuce 사용하기
2-1. Lettuce란?
Lettuce는 Redis를 Java에서 비동기적으로 사용할 수 있게 해주는 Redis 클라이언트 라이브러리로, 주로 비동기 및 반응형 프로그래밍에 강점을 가지고 있습니다. 또한, Lettuce는 Redis의 SETNX 명령어를 사용하여 분산 락을 구현합니다.
- 동작방식
- SETNX 명령어 기반의 분산 락: Lettuce는 Redis의 SETNX 명령어를 사용하여 분산 락을 구현합니다. SETNX(Set if Not Exists) 명령어는 지정된 키가 존재하지 않을 때에만 값을 설정하므로, 락을 획득하려는 프로세스가 SETNX를 통해 해당 키를 설정하면 락을 얻을 수 있습니다. 만약 키가 이미 존재한다면, 다른 프로세스가 이미 락을 가지고 있는 것이므로 SETNX는 실패합니다.
- Spin Lock 방식: Lettuce의 락 구현은 Spin Lock 방식으로 작동합니다. 락을 획득하지 못한 프로세스는 계속해서 SETNX 명령어를 반복적으로 호출하여 락을 얻을 수 있을 때까지 시도합니다. 이 과정에서 CPU 자원을 지속적으로 사용합니다.
- TTL(Time To Live) 설정: 락이 획득된 후, EXPIRE 명령어를 사용하여 락에 TTL을 설정합니다. 이를 통해 프로세스가 비정상 종료되거나 락을 해제하지 못하는 상황이 발생하더라도, 일정 시간이 지나면 락이 자동으로 해제되도록 할 수 있습니다.

- 사용 시 주의사항
- Spin Lock으로 인한 CPU 부하: Spin Lock 방식은 락을 획득할 때까지 지속적으로 CPU를 사용하여 반복 시도하기 때문에, CPU 부하가 증가할 수 있습니다. 따라서 무한 대기 상태를 방지하기 위해 타임아웃을 설정하거나, 재시도 간의 딜레이를 적절히 조절하는 것이 중요합니다.
- TTL 설정의 중요성: TTL이 설정되지 않으면 락이 영구적으로 유지될 수 있으며, 다른 프로세스들이 무기한 대기하게 될 수 있습니다. 반대로 TTL이 너무 짧게 설정되면 작업이 완료되기 전에 락이 해제되어 데이터 무결성이 손상될 위험이 있습니다.
- 장점
- 비동기 및 반응형 프로그래밍 지원: Lettuce는 비동기 및 반응형 API를 제공하므로, I/O 바운드 작업이 많은 애플리케이션에서 높은 성능을 발휘할 수 있습니다.
- 간단한 락 구현: SETNX와 EXPIRE 명령어를 사용한 간단한 분산 락 구현이 가능하여, 필요한 경우 쉽게 락 메커니즘을 사용할 수 있습니다.
- 높은 유연성: Lettuce는 동기, 비동기, 반응형 프로그래밍 모델을 모두 지원하므로, 다양한 애플리케이션 요구 사항에 맞춰 Redis를 사용할 수 있습니다.
- 단점
- Spin Lock의 비효율성: Spin Lock 방식으로 인해 락을 획득하는 동안 CPU 자원이 과도하게 소모될 수 있습니다. 이는 특히 고부하 환경에서 문제가 될 수 있습니다.
- 복잡한 예외 처리: 비동기 및 반응형 프로그래밍 모델을 사용하면 예외 처리가 복잡해질 수 있으며, 이를 관리하는 코드가 복잡해질 수 있습니다.
- TTL 설정 관리: 락의 TTL 설정이 적절하지 않을 경우, 작업 완료 전에 락이 해제되거나 반대로 락이 과도하게 오래 유지되어 자원 낭비를 초래할 수 있습니다.
2-2. Lettuce를 사용하는 방법
먼저, Redis를 사용하기 위해 Docker에서 Redis 이미지를 다운로드하고, Redis를 실행시켜야 합니다.
docker ps 명령어로 Docker가 실행 중인지 확인해 주시고, Redis 이미지 다운로드 후, Redis를 실행하겠습니다.
필요한 명령어는 아래에 적어두겠습니다!
컨테이너 실행 상태 확인하기 : docker ps
redis 이미지 다운받기 : docker pull redis
redis 실행하기 : docker run --name 이름 -d -p 6379:6379 redis
redis cli 실행하기 : docker exec -it 컨테이너ID redis-cli
redis cli까지 실행시켰다면, 다시 IDE로 돌아가서 Lettuce를 사용하기 위해 로직을 구현하겠습니다.
Redis 명령어를 사용해야 하기 때문에(setnx), RedisLockRepository 클래스를 생성하겠습니다.
@Component
public class RedisLockRepository {
private RedisTemplate<String, String> redisTemplate;
public RedisLockRepository(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public Boolean lock(Long key) {
return redisTemplate
.opsForValue()
.setIfAbsent(generateKey(key), "lock", Duration.ofMillis(3_000));
}
public Boolean unlock(Long key) {
return redisTemplate.delete(generateKey(key));
}
private String generateKey(Long key) {
return key.toString();
}
}
Lettuce 실행 순서를 정리하자면, 아래와 같습니다.
- Spin Lock 방식으로 락 얻기를 시도
- 락을 얻은 후, 재고 감소 로직을 처리
- 로직 처리 후, 락을 해제
이 순서로 Facade 클래스에 락 획득 성공 시 로직과 해제 로직을 작성하겠습니다.
@Component
public class LettuceLockStockFacade {
private final RedisLockRepository redisLockRepository;
private final StockService stockService;
public LettuceLockStockFacade(RedisLockRepository redisLockRepository, StockService stockService) {
this.redisLockRepository = redisLockRepository;
this.stockService = stockService;
}
public void decrease(Long id, Long quantity) throws InterruptedException {
while (!redisLockRepository.lock(id)) {
Thread.sleep(100);
}
try {
stockService.decrease(id, quantity);
} finally {
redisLockRepository.unlock(id);
}
}
}
while문 안에서 락 획득을 시도하는데, Spin Lock 방식이 Redis에 주는 부하를 줄여주기 위해서 Thread sleep을 설정해 줍니다.
그리고 락 획득에 성공하면, 재고 감소 로직을 실행하고, 로직 실행 후 락을 해제합니다.
아래의 테스트로 Lettuce를 활용한 재고 감소 로직이 정상 동작하는지 확인하겠습니다.
@SpringBootTest
class LettuceLockStockFacadeTest {
@Autowired
private LettuceLockStockFacade lettuceLockStockFacade;
@Autowired
private StockRepository stockRepository;
@BeforeEach
public void before() {
stockRepository.saveAndFlush(new Stock(1L, 100L));
}
@AfterEach
public void after() {
stockRepository.deleteAll();
}
@Test
public void 동시에_100개의_요청() throws InterruptedException {
int threadCount = 100;
ExecutorService executorService = Executors.newFixedThreadPool(32);
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
executorService.submit(() -> {
try {
lettuceLockStockFacade.decrease(1L, 1L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
latch.countDown();
}
});
}
latch.await();
Stock stock = stockRepository.findById(1L).orElseThrow();
// 100 - (1 * 100) = 0
assertEquals(0, stock.getQuantity());
}
}

테스트케이스가 정상 통과되는 것을 확인할 수 있습니다. 소요시간은 29.126초인데... 이게 제 컴퓨터가 똥컴이라 그런지는 잘 모르겠지만 일단 시간이 좀 많이 소요되었습니다...
3. Redis의 Redisson 사용하기
3-1. Redisson란?
Redisson은 Redis를 활용한 고급 Java 클라이언트 라이브러리로, 분산 락을 포함한 다양한 분산 데이터 구조와 서비스들을 제공합니다. Redisson의 분산 락 구현은 Redis의 기본 명령어뿐만 아니라, pub/sub 메커니즘을 활용하여 보다 효율적이고 안정적인 락 관리를 제공합니다.
- 동작방식
- pub/sub 기반 락 구현: Redisson의 락 구현은 Redis의 pub/sub(publish/subscribe) 메커니즘을 사용하여 락의 상태를 모니터링하고, 락이 해제되면 다른 클라이언트가 이를 감지할 수 있도록 합니다. 이를 통해 락을 기다리는 클라이언트들이 주기적으로 Redis에 락 상태를 확인하지 않고도 락을 획득할 수 있습니다. 이는 Spin Lock 방식의 단점을 보완하는 메커니즘입니다.
- 분산 락 및 재진입 가능 락: Redisson은 RLock이라는 인터페이스를 제공하며, 이 락은 재진입 가능(Reentrant)합니다. 즉, 동일한 스레드에서 여러 번 락을 획득할 수 있고, 락이 획득된 횟수만큼 해제할 수 있습니다.
- 고급 기능: Redisson은 분산 락 외에도 공정 락(Fair Lock), 읽기/쓰기 락(Read/Write Lock), 세마포어(Semaphore), 카운트다운 래치(CountDown Latch) 등의 다양한 동시성 제어 메커니즘을 제공합니다.

- 사용 시 주의사항
- 의존성 추가: Redisson은 별도의 라이브러리로 제공되므로, Maven 또는 Gradle과 같은 빌드 도구를 사용하여 프로젝트에 의존성을 추가해야 합니다. 이는 추가적인 설정 작업을 필요로 합니다.
- Redis의 단일 장애 지점(Single Point of Failure): Redisson은 Redis에 의존하기 때문에, Redis가 단일 장애 지점이 될 수 있습니다. 이를 방지하기 위해 Redis 클러스터나 Sentinel을 구성하여 고가용성을 확보하는 것이 중요합니다.
- 적절한 락 설정: 락의 TTL(Time To Live) 및 다른 설정들을 신중하게 다루어야 합니다. 적절하지 않은 설정은 락 해제 문제나 시스템 리소스 낭비를 초래할 수 있습니다.
- 장점
- 효율적인 락 관리: pub/sub 메커니즘을 활용하여 효율적인 락 관리를 제공하며, Spin Lock 방식의 CPU 부하 문제를 줄일 수 있습니다.
- 다양한 동시성 제어 도구: Redisson은 단순한 락 이외에도 다양한 고급 동시성 제어 도구를 제공하여 복잡한 분산 환경에서도 쉽게 사용할 수 있습니다.
- 높은 유연성: Redisson은 Java 표준 인터페이스를 사용하여 Redis와의 상호작용을 쉽게 만듭니다. 이를 통해 기존 Java 코드와의 호환성이 높습니다.
- 단점
- 추가적인 라이브러리 의존성: 프로젝트에 별도의 Redisson 라이브러리를 추가해야 하며, 이는 추가적인 설정 및 관리 작업을 필요로 합니다.
- Redis에 대한 의존성: Redis에 의존하기 때문에, Redis 서버가 단일 장애 지점이 될 수 있으며, Redis의 성능이나 안정성에 영향을 받을 수 있습니다.
- 복잡성: 다양한 기능을 제공하는 만큼 설정이 복잡할 수 있으며, 이를 제대로 이해하고 설정하지 않으면 예상치 못한 문제가 발생할 수 있습니다.
3-2. Redisson을 사용하는 방법
먼저, Redisson 라이브러리를 사용하기 위해서는, Redis 말고도 별도의 의존성을 추가해줘야 합니다. 저는 Gradle을 사용하고 있는데, Maven을 사용하고 있다면 아래 maven repository 사이트에서 버전을 선택해서 의존성을 주입하면 됩니다.
https://mvnrepository.com/artifact/org.redisson/redisson-spring-boot-starter
dependencies {
implementation group: 'org.redisson', name: 'redisson-spring-boot-starter', version: '3.31.0'
}
Redisson을 사용하면, Lettuce처럼 별도의 리포지토리를 작성할 필요 없이, Redisson의 Lock 관련 클래스를 활용하면 됩니다.
그래도 재고 감소 로직 전 후로 락 획득 관련 로직이 필요하기 때문에, Facade 작성을 하겠습니다.
@Component
public class RedissonLockStockFacade {
private RedissonClient redissonClient;
private StockService stockService;
public RedissonLockStockFacade(RedissonClient redissonClient, StockService stockService) {
this.redissonClient = redissonClient;
this.stockService = stockService;
}
public void decrease(Long id, Long quantity) {
RLock lock = redissonClient.getLock(id.toString());
try {
boolean available = lock.tryLock(15, 1, TimeUnit.SECONDS);
if (!available) {
System.out.println("lock 획득 실패");
return;
}
stockService.decrease(id, quantity);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}
아래와 같은 순서로 동작하도록 decrease 메서드를 작성해 줬습니다.
- redissonClient를 활용해서 lock 객체를 가져옵니다.
- 몇 초동안 락을 획득을 시도할 건지, 락을 몇 초 동안 점유할 건지를 설정해 줍니다.
- 만약 락 획득에 실패했다면, 로그를 남겨준 후에 return 합니다.
- 정상적으로 락 획득에 성공하면, 재고 감소 로직을 실행합니다.
- 로직이 정상적으로 실행 완료되면, 락을 해제합니다.
Lettuce를 테스트했던 테스트케이스와 동일하게 코드를 작성해서 Redisson으로 재고 감소 로직이 정상 동작하는지 확인하겠습니다.

테스트케이스가 정상 통과되는 것을 확인할 수 있습니다. 소요시간은 3.174초로 나오는데, 인강 강사님은 Redisson방식도 10초가 넘어가는 걸로 봐서는 각 컴퓨터 환경마다 소요시간이 다르게 나오는 것 같습니다. 참고!
4. Lettuce와 Redisson 비교
Lettuce
- 구현이 간단하다: Lettuce는 구현이 비교적 간단하며, SETNX 명령어와 Spin Lock 방식을 사용해 락을 구현합니다. 이러한 단순한 구조는 빠르게 락을 적용할 수 있는 장점이 있습니다.
- Spring Data Redis와의 통합: Spring Data Redis를 사용하는 경우, Lettuce는 기본 Redis 클라이언트로 설정되기 때문에 별도의 라이브러리를 추가할 필요가 없습니다. 이는 개발 과정에서 편의성을 제공합니다.
- Spin Lock 방식의 부하: Lettuce는 Spin Lock 방식을 사용하기 때문에, 많은 스레드가 동시에 락을 획득하려고 대기하는 경우 Redis 서버에 부하가 발생할 수 있습니다. 이는 특히 고부하 환경에서 성능 문제를 야기할 수 있습니다.
Redisson
- 락 획득 재시도 기능: Redisson은 락 획득 재시도를 기본으로 제공하여, 락을 획득하지 못한 경우에도 자동으로 재시도하는 기능을 갖추고 있습니다. 이를 통해 락 획득 실패 시의 복잡한 재시도 로직을 직접 구현할 필요가 없습니다.
- pub/sub 기반의 효율성: Redisson은 pub/sub 방식으로 락을 관리하기 때문에, Lettuce의 Spin Lock 방식에 비해 Redis 서버에 가해지는 부하가 적습니다. 이는 특히 대규모 트래픽 환경에서 성능을 더 효율적으로 관리할 수 있게 해 줍니다.
- 별도의 라이브러리 필요: Redisson은 Lettuce와 달리 별도의 라이브러리로 제공되기 때문에, 프로젝트에 추가적인 의존성을 포함시켜야 합니다. 이로 인해 초기 설정 작업이 필요할 수 있습니다.
- 락 기능 학습 필요: Redisson은 다양한 락 기능과 분산 데이터 구조를 제공하지만, 이를 효과적으로 사용하기 위해서는 학습이 필요합니다. Redisson의 다양한 기능을 제대로 이해하고 활용하기 위해서는 추가적인 공부가 요구됩니다.
정리하자면, 재시도가 필요하지 않은 Lock이라면 Lettuce를 활용하고, 재시도가 필요한 경우에는 Redisson을 활용하는 방식으로 실무에서 사용한다고 합니다.
5. 정리 (MySQL과 Redis를 활용한 동시성 제어 비교)
저번 글과 이번 글에서 MySQL과 Redis를 활용해서 동시성 제어를 하고, 사용하는 방법을 정리해 봤습니다. 마지막으로, MySQL과 Redis를 활용하는 방법을 비교하면서 이번 글을 마무리하겠습니다!
📌 MySQL
- 비용 효율성: 이미 MySQL을 사용하고 있는 환경이라면, MySQL의 Lock 메커니즘을 추가적인 비용 없이 활용할 수 있습니다. 추가적인 인프라나 라이브러리 설치가 필요하지 않습니다.
- 적절한 트래픽 처리: MySQL의 Lock 메커니즘은 적절한 수준의 트래픽에서는 안정적으로 동작하며, 데이터베이스의 일관성과 무결성을 유지하는 데 효과적입니다.
- 성능 한계: 그러나 Redis와 비교했을 때, MySQL은 대규모 트래픽 환경에서 성능이 떨어질 수 있습니다. 특히, 고성능이 요구되는 상황에서는 병목 현상이 발생할 수 있습니다.
📌 Redis
- 추가 비용 발생 가능성: Redis를 활용하고 있지 않은 환경이라면, Redis 인스턴스를 구축하고 관리하는 데 추가적인 비용과 인프라 관리 비용이 발생할 수 있습니다. Redis는 별도의 설치 및 설정이 필요하며, 고가용성을 위해 클러스터링이나 Sentinel 구성이 필요할 수 있습니다.
- 우수한 성능: Redis는 인메모리 데이터베이스로, MySQL보다 훨씬 높은 성능을 제공합니다. 특히, 낮은 레이턴시와 높은 처리량이 요구되는 환경에서 Redis는 매우 효과적입니다. Redis의 비동기 처리와 다양한 고급 기능들은 대규모 트래픽을 처리하는 데 탁월한 선택이 될 수 있습니다.
보통 실무에서는 비용적 여유가 없거나, MySQL로 처리가 가능할 정도의 트래픽이라면 MySQL을 활용하고,
비용적 여유가 있거나 MySQL로는 처리가 불가능할 정도의 트래픽이 발생한다면 Redis를 도입한다고 합니다~
동시성 문제의 근본적인 해결방법은 데이터에 순차적으로 접근해서 통제하는 방식을 사용하는 것이고, 상황에 따라서 알맞은 방법을 선택해서 동시성 제어를 하면 된다! 입니다.
제 정리가 맞는 건지... 는 모르겠지만~ 두 게시물로 정리한 동시성 제어 방법은 여기서 마무리하겠습니다. 그럼 안녕!
🍀
좋아하는 것을 계속 좋아하세요!
반드시 행복해집니다
[Github] https://github.com/chujaeyeong
[E-mail] chujy1224@gmail.com
'Study > Spring' 카테고리의 다른 글
| Spring MVC에서 사용자 입력 검증(Validation)과 HTML5 유효성 검사의 차이점 (6) | 2024.11.06 |
|---|---|
| MySQL에서 제공하는 Lock을 이용해서 동시성 제어하기 (Pessimistic Lock, Optimistic Lock, Named Lock) (0) | 2024.08.17 |
| 선착순 쿠폰 발급 시스템 로직 변경으로 Redis의 Set 자료구조 찍먹해보기 (0) | 2024.07.02 |
| 선착순 쿠폰 발급 시스템 개발을 통해 Redis 랑 Kafka 찍먹해보기 (2) | 2024.07.01 |
| [Spring] Validation (검증) 처리 방법 및 properties 파일의 한글이 출력되지 않는 문제 해결 방법 (2) | 2023.10.12 |