배경 및 목표
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을 선택한 이유는?
Related Posts
Search