JPA 기반의 비관적 락을 통해 단일 서버 환경에서의 중복 예약 문제는 효과적으로 해결할 수 있었다. 하지만 서비스가 멀티 인스턴스 환경으로 확장되면 이야기가 달라진다. 각 서버 인스턴스는 서로 다른 JVM에서 동작하기 때문에, JPA 수준의 락은 인스턴스 간에 공유되지 않는다. 결국 같은 좌석에 대해 서로 다른 서버에서 동시에 예약 요청이 들어오면, 데이터 정합성을 보장할 수 없게 된다.
이 문제를 해결하기 위해, 우리는 Redis 기반의 분산 락을 적용했다. Redisson은 Redis를 기반으로 분산 락, 세마포어, 카운트다운 래치 등을 제공하는 고수준의 동기화 도구이다. 이번 글에서는 Redisson의 RLock
을 활용해 멀티 서버 환경에서도 중복 예약을 방지하는 구조를 구현하고 테스트한 경험을 정리한다.
Redisson에서 제공하는 RLock
은 다음과 같은 구조로 사용할 수 있다:
val lock: RLock = redissonClient.getLock("lock:seat:$seatId")
lock.tryLock(3, 10, TimeUnit.SECONDS)
락을 획득하면, 이 트랜잭션이 먼저 좌석 예약 여부를 검사하고, 예약 데이터를 저장한다. 락을 획득하지 못한 요청은 일정 시간 대기 후 타임아웃된다.
사용자 요청 → 서버 A --------------┐
→ 락 획득 시도 │
│
사용자 요청 → 서버 B --------------┘
→ 락 획득 실패 (대기 or 실패 처리)
tryLock()
타임아웃이 만료될 때까지 대기이 흐름은 단일 서버에서 동작하던 JPA 비관적 락과는 달리, 분산 환경에서 서버 간의 동기화 수단으로 동작한다.
Redisson의 tryLock()
은 락 획득 실패 시 null이나 예외를 반환하지 않고 false를 반환하므로, 이에 대한 분기 처리가 반드시 필요하다:
if (!lock.tryLock(3, 10, TimeUnit.SECONDS)) {
throw ReservationFailedException("잠시 후 다시 시도해주세요.")
}
또한 락 해제는 반드시 finally 블록에서 수행되어야 한다:
finally {
if (lock.isHeldByCurrentThread) {
lock.unlock()
}
}
Redisson 내부적으로는 Lua 스크립트를 통해 Redis에 원자적으로 명령을 실행하므로, 락 해제 시 다른 인스턴스의 락을 실수로 해제하는 문제도 방지된다.