예약 시스템 락(Lock) 기술적 의사결정 - fitpassTeam/fitpass GitHub Wiki

문제 : 동시 예약 요청에서 발생하는 레이스 컨디션

동시성 제어 도입 배경

  • 비즈니스 크리티컬: 체육관 PT 예약은 1:1 매칭으로 중복 예약이 절대 발생하면 안 됨
  • 레이스 컨디션 발생: 동일한 트레이너의 같은 시간대에 여러 사용자가 동시 예약 시도
  • 데이터 무결성 위험: 중복 예약 발생 시 고객 불만 및 운영상 문제 야기
  • 확장성 고려: 서비스 성장 시 더 많은 동시 요청 처리 필요

문제 시나리오

시나리오: 인기 트레이너 김코치의 오후 2시 예약
1. 사용자A: 14:00 예약 요청 → 중복 체크 → 없음 → 예약 진행
2. 사용자B: 14:00 예약 요청 → 중복 체크 → 없음 → 예약 진행 (동시 실행)
3. 결과: 같은 시간에 2개의 예약이 생성됨

의사결정 배경 및 요구사항

비즈니스 요구사항

  • 완벽한 동시성 제어: 1:1 예약 특성상 100% 중복 방지 필요
  • 사용자 경험: 실패 시 명확한 안내 메시지 제공
  • 성능: 예약 과정이 지나치게 느려지면 안 됨

기술적 요구사항

  • 확장성: 멀티 서버 환경에서도 동작
  • 성능: 동시 처리량 확보
  • 안정성: 락 해제 실패 시에도 시스템 정상 동작
  • 유지보수성: 코드 복잡도 최소화

Redis 분산 락 (Distributed Lock)

@Service
@RequiredArgsConstructor
public class ReservationService {
    
    private final RedissonClient redissonClient;
    
    public ReservationResponseDto createReservation(
        LocalDate reservationDate, LocalTime reservationTime,
        Long userId, Long gymId, Long trainerId) {
        
        // 세밀한 락 키 생성
        String lockKey = String.format("reservation:lock:%d:%s:%s",
            trainerId, reservationDate, reservationTime);
        
        RLock lock = redissonClient.getLock(lockKey);
        
        try {
            // 락 획득 시도 (10초 대기, 30초 후 자동 해제)
            if (!lock.tryLock(10, 30, TimeUnit.SECONDS)) {
                throw new BaseException(ExceptionCode.RESERVATION_ALREADY_EXISTS);
            }
            
            // 안전한 예약 로직 실행
            return reservationCreate(reservationDate, reservationTime, 
                ReservationStatus.PENDING, userId, gymId, trainerId);
                
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new BaseException(ExceptionCode.RESERVATION_INTERRUPTED);
        } finally {
            // 안전한 락 해제
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

장점:

  • 멀티 서버 대응: 분산 환경에서 완벽한 동시성 제어
  • 세밀한 제어: 트레이너별, 날짜별, 시간별 독립적 락
  • 높은 성능: 관련 없는 예약은 동시 처리 가능
  • 좋은 사용자 경험: 대기 후 명확한 결과 안내
  • 자동 해제: 서버 다운 시에도 락 자동 해제로 데드락 방지

단점:

  • 외부 의존성: Redis 서버 필요
  • 구현 복잡도: 락 관리 로직 추가
  • 네트워크 지연: Redis 통신으로 인한 약간의 지연

선택한 이유 :

"세밀한 제어가 가능하면서도 확장성과 성능을 모두 확보할 수 있는 Redis 분산 락을 선택”


Redis 분산 락 결정사항

  1. 비즈니스 요구사항 최적 부합
  2. 세밀한 락 범위 제어
// 락 키 설계 전략
"reservation:lock:{trainerId}:{date}:{time}"
  1. 사용자 경험 최적화
// 사용자 관점에서의 플로우
1. 예약 요청 클릭
2. "처리 중..." 표시 (락 대기)
3. 성공: "예약이 완료되었습니다!"
   실패: "다른 사용자가 먼저 예약했습니다. 다른 시간을 선택해주세요."

vs. 다른 방식들:
- DB 락: "시스템이 느려요..."
- 낙관적 락: "다시 시도해주세요" (재입력 필요)
- 유니크 제약: "에러가 발생했습니다"

구현 세부사항

1. Redisson 설정

@Configuration
public class RedissonConfig {
    
    @Value("${spring.redis.host}")
    private String redisHost;
    
    @Value("${spring.redis.port}")
    private int redisPort;
    
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
            .setAddress("redis://" + redisHost + ":" + redisPort)
            .setRetryAttempts(3) // 재시도 횟수
            .setRetryInterval(1500) // 재시도 간격
            .setTimeout(3000) // 타임아웃
            .setConnectTimeout(10000); // 연결 타임아웃
            
        return Redisson.create(config);
    }
}

2. 락 키 생성

// 서비스에 직접 구현
String lockKey = String.format("reservation:lock:%d:%s:%s",
    trainerId, reservationDate, reservationTime);

3. 락 범위의 정교함

: 효과적으로 락을 분리하여 독립적으로 관리

4. 타임아웃 설정

// 실용적인 타임아웃 설정
lock.tryLock(10, 30, TimeUnit.SECONDS)
//           ↑   ↑
//    대기시간   락유지시간

이중 보안 체계(Defense in Depth)

1차 방어선 : Redis 분산 락

// 사용자 경험 최적화 + 성능 확보
if (!lock.tryLock(10, 30, TimeUnit.SECONDS)) {
    throw new BaseException(ExceptionCode.RESERVATION_ALREADY_EXISTS);
}

2차 방어선 : DB 유니크 제약조건

// 최종 안전장치 (Redis 장애 시에도 데이터 무결성 보장)
@Entity
@Table(uniqueConstraints = @UniqueConstraint(
    columnNames = {"trainer_id", "reservation_date", "reservation_time"}
))
public class Reservation { ... }

성능 검증 결과

동시성 테스트

// 실제 테스트 시나리오
- 10명 동시 → 성공 1명, 실패 9명 ✅
- 50명 동시 → 성공 1명, 실패 49명 ✅  
- 100% 동시성 제어 달성 ✅

(성능 테스트는 따로 올렸습니다)

배운점

  • 100% 중복 방지: 어떤 상황에서도 중복 예약 차단
  • 이중 보안 체계: Redis + DB 유니크 제약조건
  • 사용자 경험 개선: 대기 및 재시도 로직

DB 제약조건만으로는 동시성 제어를 한 것이 아니고 Redis와 같이하여 안정성도 올리고, 락의 다양함을 배웠다.


예약 시스템 락(Lock) 기술적 의사결정