실전 고수 팁 — 동시성 제어를 위한 Redis 분산 락(Distributed Lock) 활용
수평 확장(Scale-Out)된 서버 환경에서는 Java의 synchronized 키워드나 ReentrantLock 같은 단일 JVM 내부의 락(Lock)은 아무런 효력이 없습니다. 서로 다른 물리 서버가 동시에 같은 DB 레코드에 접근하는 문제는 **Redis 분산 락(Distributed Lock)**으로 해결해야 합니다.
1. Redisson(레디슨)을 이용한 분산 락
Redisson 라이브러리는 Redis 위에 구현된 자바 친화적인 분산 락을 제공합니다. tryLock() 호출 시 첫 번째 서버만 락을 획득하고, 다른 서버들은 락이 풀릴 때까지 대기합니다.
implementation 'org.redisson:redisson-spring-boot-starter:3.29.0'
@Service
@RequiredArgsConstructor
@Slf4j
public class CouponService {
private final RedissonClient redissonClient;
private final CouponRepository couponRepository;
public void issueCoupon(Long userId) {
String lockKey = "coupon:issue:lock:" + userId;
RLock lock = redissonClient.getLock(lockKey);
// 최대 5초 대기 후 락 획득 시도, 획득하면 3초간 유지 후 자동 해제
boolean acquired = false;
try {
acquired = lock.tryLock(5, 3, TimeUnit.SECONDS);
if (!acquired) {
throw new IllegalStateException("현재 다른 요청이 처리 중입니다. 잠시 후 다시 시도해주세요.");
}
// 락을 가진 스레드만 여기에 진입합니다.
Coupon coupon = couponRepository.findAvailable()
.orElseThrow(() -> new IllegalStateException("쿠폰 수량이 소진되었습니다."));
coupon.assignTo(userId);
couponRepository.save(coupon);
log.info("쿠폰 발급 성공: userId={}", userId);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 반드시 finally에서 락 해제! 예외 발생 시에도 락 반납 보장
if (acquired && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
2. Redis 락 vs DB 락(비관적 락) 비교
| 기준 | Redis 분산 락(Redisson) | DB 비관적 락(@Lock(PESSIMISTIC_WRITE)) |
|---|---|---|
| 성능 | 매우 빠름 (메모리 연산) | 느림 (DB 커넥션 점유, 행 수준 락) |
| 외부 의존 | Redis 서버 필요 | 별도 인프라 불필요 |
| 인프라 장애 시 | Redis 장애 시 전체 락 불가 | DB 장애 시 락도 같이 중단 |
| 사용 권장 | 대량 트래픽 동시성 제어 | 소규모 단순 비즈니스 정합성 보장 |
팁
락의 TTL(만료 시간)을 너무 짧게 잡으면 비즈니스 로직이 완료되기 전 락이 풀려 동시성 문제가 재발합니다. 예상 처리 시간의 최소 3배 이상의 TTL을 설정하고, Redisson의 watchdog 기능(자동 TTL 연장)을 고려하세요.