Search

[Applicaion]08.동시성 문제: 방안(2) Redis Lock

Publish Date
2024/11/13
Category
Tags
Status
Done
1 more property
동시성 문제: Redis Lock
멀티 서버 환경에서 쿠폰 발급의 동시성 문제를 해결하기 위해 Redis 기반 분산 락을 적용하여 데이터 정합성을 보장하는 것을 목표로 한다.

배경 및 목표

Java Synchronized는 자바 애플리케이션 내에서만 동작하므로 단일 서버 환경에서만 동작하므로, 서버를 여러 대 확장(Scale-out)했을 때는 관리가 어렵다. 이를 해결하기 위해 Redis를 이용한 분산 락을 도입한다. 분산 락은 여러 서버 간 동일한 자원에 대해 동시 접근을 제어하는 메커니즘이다. 락을 획득한 프로세스만 작업을 진행할 수 있으며, 작업 완료 후 락을 해제한다. 이를 통해 멀티 서버 환경에서도 데이터 정합성을 보장한다. 즉, Java Synchronized의 단일 서버 한계를 극복하고 멀티 서버 환경에서도 쿠폰 발급의 데이터 정합성을 보장하기 위해 Redis 기반의 분산 락을 도입한다.

분산 락이란?

분산 락은 여러 서버 간 동일한 자원에 대해 동시 접근을 제어하는 메커니즘으로, 락을 획득한 프로세스만 작업을 진행하고, 완료 후 락을 해제한다. 이를 통해 멀티 서버 환경에서도 동시성 문제를 해결할 수 있다.

Redisson 활용 구조

Redisson은 Redis를 이용한 분산 락을 제공하며, tryLock과 같은 메서드를 통해 락 획득 및 해제가 가능하다.
tryLock 방식은 락을 획득할 수 없는 상황에서도 설정된 시간 동안 재시도하고, 타임아웃을 설정해 무한 대기 상황을 방지한다.

Redis Lock 구현

Redis 기반의 분산 락멀티 서버 환경에서 동시성 제어를 통해 데이터 정합성을 보장하는 해결책이다.
DistributeLockExecutor는 Redis 락을 통해 하나의 프로세스만 자원에 접근하도록 제어한다.
락을 tryLock 방식으로 구현하여 타임아웃 설정오류 처리를 통해 성능과 안정성을 높였다.
이를 통해 쿠폰 발급 서비스안전하고 효율적으로 동시 접근을 제어할 수 있다.

구조 및 구현

coupon-BE/ ├── build.gradle.kts # Redis 의존성 추가 └── coupon-core/ ├── resources/ │ └── application-core.yml # Redis 설정 포함 애플리케이션 프로퍼티 └── src/ └── main/ └── java/com/example/couponcore/ ├── component/ │ └── DistributeLockExecutor.java # Redis 기반 분산 락 구현 클래스 ├── configuration/ │ └── RedisConfiguration.java # Redis 클라이언트 설정 클래스 └── service/ └── CouponIssueRequestService.java # 쿠폰 발급 요청 처리 서비스
Java
복사
파일명
내용
build.gradle.kts
Redis 의존성을 추가하여 프로젝트에 통합
application-core.yml
Redis 설정(host, port) 및 테스트 환경 구성 파일
RedisConfiguration.java
Redis 클라이언트(RedissonClient) 설정 및 Bean 생성
DistributeLockExecutor.java
Redis 기반 분산 락 구현, 락 획득 후 지정된 비즈니스 로직 실행 관리 클래스
CouponIssueRequestService.java
쿠폰 발급 요청 시 Redis 락을 적용해 동시성 문제를 제어하는 서비스

소스코드

build.gradle.kts
Redis 클라이언트를 프로젝트에 통합하기 위해 필요한 의존성을 추가한다.
dependencies { implementation ("org.springframework.boot:spring-boot-starter-data-redis") }
Java
복사
의존성
설명
spring-boot-starter-data-redis
Spring Boot Redis 연동 라이브러리
application-core.yml
Redis 설정 및 테스트 환경 설정을 포함하여 애플리케이션의 환경 정보를 정의한다.
spring: data: redis: host: localhost port: 6379
Java
복사
필드
설명
host
Redis 서버의 호스트 주소
port
Redis 서버의 포트 번호
RedisConfiguration.java
Redis 클라이언트를 설정하고 RedissonClient를 Bean으로 등록하여 애플리케이션 내에서 사용 가능하도록 한다.
package com.example.couponcore.configuration; @Configuration public class RedisConfiguration { @Value("${spring.data.redis.host}") private String host; @Value("${spring.data.redis.port}") private int port; @Bean RedissonClient redissonClient() { Config config = new Config(); String address = "redis://" + host + ":" + port; config.useSingleServer().setAddress(address); return Redisson.create(config); } }
Java
복사
필드/메서드
설명
@Value host, port
Redis 서버의 호스트와 포트 값을 주입받음
redissonClient
Redis 클라이언트를 초기화하고 Bean으로 등록
DistributeLockExecutor.java
Redis 기반 락을 생성하고 관리하며, 락을 성공적으로 획득한 경우에만 지정된 로직을 실행하도록 제어한다.
package com.example.couponcore.component; import lombok.RequiredArgsConstructor; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import java.util.concurrent.TimeUnit; @RequiredArgsConstructor @Component public class DistributeLockExecutor { private final RedissonClient redissonClient; private final Logger log = LoggerFactory.getLogger(this.getClass().getSimpleName()); public void execute(String lockName, long waitMilliSecond, long leaseMilliSecond, Runnable logic) { RLock lock = redissonClient.getLock(lockName); // Redis 락 객체 생성 try { boolean isLocked = lock.tryLock(waitMilliSecond, leaseMilliSecond, TimeUnit.MILLISECONDS); if (!isLocked) { // 락 획득 실패 throw new IllegalStateException("[" + lockName + "] lock 획득 실패"); } logic.run(); // 락을 성공적으로 획득하면 로직 실행 } catch (InterruptedException e) { throw new RuntimeException(e); } finally { if (lock.isHeldByCurrentThread()) { // 현재 스레드가 락을 가지고 있다면 해제 lock.unlock(); } } } }
Java
복사
메서드/필드
설명
execute
락을 시도하고 성공 시 지정된 로직 실행
redissonClient.getLock
주어진 이름으로 Redis 락 객체를 생성
lock.tryLock
대기 시간 내 락 획득 시도
lock.unlock
현재 스레드가 소유한 락을 해제
Logger
락 획득 및 해제와 관련된 메시지를 기록
CouponIssueRequestService.java
Redis 기반 락을 사용하여 쿠폰 발급 로직의 동시성 문제를 해결한다.
package com.example.couponapi.service; import com.example.couponcore.component.DistributeLockExecutor; import lombok.RequiredArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @RequiredArgsConstructor @Service public class CouponIssueRequestService { private final DistributeLockExecutor distributeLockExecutor; private final CouponIssueService couponIssueService; private final Logger log = LoggerFactory.getLogger(this.getClass().getSimpleName()); public void issueRequestV1(CouponIssueRequestDto requestDto) { distributeLockExecutor.execute("lock_" + requestDto.couponId(), 1000, 1000, () -> { couponIssueService.issue(requestDto.couponId(), requestDto.userId()); }); log.info("쿠폰 발급 완료. couponId: %s, userId: %s".formatted(requestDto.couponId(), requestDto.userId())); } }
Java
복사
메서드/필드
설명
distributeLockExecutor.execute
Redis 락 획득 후 쿠폰 발급 로직 실행
"lock_" + requestDto.couponId()
각 쿠폰에 대해 고유한 락 이름 지정
Logger log
쿠폰 발급 완료 또는 실패 메시지를 기록
couponIssueService.issue
쿠폰 발급 비즈니스 로직 호출

성능테스트 결론

Redis Lock (v1.2)

버전 정보

버전
변경 사항
v1.2
동시성 문제: Redis Lock - build.gradle.kts - application-core.yml - RedisConfiguration.java - DistributeLockExecutor.java - CouponIssueRequestService.java

테스트 결과

테스트 환경: Local 환경에서 성능 테스트 3회 실시, 평균 값 도출
소스코드버전정보
인프라 환경
쿠폰 발급 수량 비교(정상발급확인)
#Request
#Fails
Failures
Average(ms)
RPS
MySQLCPU(%)
Redis CPU(%)
v1.2
local
불일치
133,685
85,496
63.83%
4234.35
233.646
51.09
6.15

테스트 결과 요약

1.
발급 수량 불일치
Redis 기반 락을 적용했지만 133,685개의 요청85,496개 실패로,
동시성 제어가 완벽하지 않아 정상 발급 수량 불일치 문제가 발생했다.
2.
Failure 비율
요청의 약 63.83%에서 오류가 발생하여 기대보다 낮은 성능을 보였다.
3.
시스템 리소스 사용률
MySQL CPU 사용률: 51.09%로 트랜잭션 처리 부하가 상당히 높은 상태이다.
Redis CPU 사용률: 6.15%로 Redis 자체는 상대적으로 낮은 리소스를 소비했다.
4.
응답 시간
평균 응답 시간은 4234.35ms로, 락 획득과 트랜잭션 처리로 인한 성능 저하가 발생했다.

결론 및 개선 방향

Redis 기반 락으로 동시성 문제를 일부 해결했으나, 락 충돌과 지연으로 인한 오류율이 여전히 높다.
추가 개선:
LUA 스크립트를 사용하여 Redis 락의 원자성 강화
TTL(Time-To-Live) 설정 조정으로 락 해제 시간 최적화
비동기 분산 큐(예: Kafka, RabbitMQ) 도입을 고려하여 시스템의 안정성과 확장성 개선
Redis 락만으로는 대규모 트래픽을 처리하기에는 한계가 있어 다양한 개선 방안의 적용이 필요하다.

Q&A

Redis 분산 락은 언제 유용한가요?
Redisson tryLock의 장점은 무엇인가요?
Redis 락의 단점은 없나요?
단일 서버와 비교했을 때 성능 차이는 어떻게 되나요?
Redisson과 Lettuce 중 Redisson을 선택한 이유는?
Search
Main PageCategoryTagskkogggokkAbout MeContact