[동시성제어] Redisson으로 분산 락 적용해보기

y001·2025년 4월 15일
0
post-thumbnail

JPA 기반의 비관적 락을 통해 단일 서버 환경에서의 중복 예약 문제는 효과적으로 해결할 수 있었다. 하지만 서비스가 멀티 인스턴스 환경으로 확장되면 이야기가 달라진다. 각 서버 인스턴스는 서로 다른 JVM에서 동작하기 때문에, JPA 수준의 락은 인스턴스 간에 공유되지 않는다. 결국 같은 좌석에 대해 서로 다른 서버에서 동시에 예약 요청이 들어오면, 데이터 정합성을 보장할 수 없게 된다.

이 문제를 해결하기 위해, 우리는 Redis 기반의 분산 락을 적용했다. Redisson은 Redis를 기반으로 분산 락, 세마포어, 카운트다운 래치 등을 제공하는 고수준의 동기화 도구이다. 이번 글에서는 Redisson의 RLock을 활용해 멀티 서버 환경에서도 중복 예약을 방지하는 구조를 구현하고 테스트한 경험을 정리한다.


1. Redisson 분산 락 구조

Redisson에서 제공하는 RLock은 다음과 같은 구조로 사용할 수 있다:

val lock: RLock = redissonClient.getLock("lock:seat:$seatId")
lock.tryLock(3, 10, TimeUnit.SECONDS)
  • 첫 번째 인자(3초): 락 획득을 시도하는 최대 대기 시간
  • 두 번째 인자(10초): 락을 획득한 후 자동 해제될 때까지 유지되는 시간 (watchdog)

락을 획득하면, 이 트랜잭션이 먼저 좌석 예약 여부를 검사하고, 예약 데이터를 저장한다. 락을 획득하지 못한 요청은 일정 시간 대기 후 타임아웃된다.


2. 전체 흐름 시퀀스 다이어그램

사용자 요청 → 서버 A --------------┐
                 → 락 획득 시도   │
                               │
사용자 요청 → 서버 B --------------┘
                 → 락 획득 실패 (대기 or 실패 처리)
  • 서버 A가 락을 선점하면, 서버 B는 tryLock() 타임아웃이 만료될 때까지 대기
  • 서버 A가 좌석 예약을 완료하고 락을 해제하면, 서버 B가 다음 순서로 진행

이 흐름은 단일 서버에서 동작하던 JPA 비관적 락과는 달리, 분산 환경에서 서버 간의 동기화 수단으로 동작한다.


3. 실패 처리 및 고려 사항

Redisson의 tryLock()은 락 획득 실패 시 null이나 예외를 반환하지 않고 false를 반환하므로, 이에 대한 분기 처리가 반드시 필요하다:

if (!lock.tryLock(3, 10, TimeUnit.SECONDS)) {
    throw ReservationFailedException("잠시 후 다시 시도해주세요.")
}

또한 락 해제는 반드시 finally 블록에서 수행되어야 한다:

finally {
    if (lock.isHeldByCurrentThread) {
        lock.unlock()
    }
}

Redisson 내부적으로는 Lua 스크립트를 통해 Redis에 원자적으로 명령을 실행하므로, 락 해제 시 다른 인스턴스의 락을 실수로 해제하는 문제도 방지된다.


5. 정리: 실무 적용 팁

  • 멀티 인스턴스 환경에서는 반드시 DB 외부에 존재하는 공통 동기화 수단이 필요하다. Redis 기반의 Redisson이 실질적으로 이를 충족시켜 준다.
  • 락 이름은 반드시 유니크하게 구성해야 하며, 하나의 도메인 단위로 설계할 것
  • 락 획득 실패에 대한 UX 대응(재시도, 실패 메시지 안내 등)도 고려해야 한다

0개의 댓글