# 비관적 락과 낙관적 락, 그리고

예약 시스템에서의 동시성 문제 해결 전략

> ReservationSlot 단위 비관적 락을 통한 정원 초과 및 중복 생성 방지

---

## 1\. 예약 시스템에서 실제로 어떤 동시성 문제가 발생했는가?

이 예약 시스템은 다음 조건을 가진다.

* 같은 사진관
    
* 같은 날짜
    
* 같은 시간대
    

에 대해 **여러 사용자가 동시에 예약 요청**을 보낼 수 있다.

이때 락이 없다면 다음 문제가 발생할 수 있다.

### 1️⃣ 정원 초과 문제

```plaintext
정원 5명
A 요청: 현재 예약 4명 → OK
B 요청: 현재 예약 4명 → OK
→ 결과: 예약 6명 (초과)
```

### 2️⃣ 슬롯 중복 생성 문제

슬롯이 아직 생성되지 않은 상태에서:

```plaintext
A 요청: 슬롯 없음 → 생성
B 요청: 슬롯 없음 → 생성
→ 동일한 (photoLab, date, time) 슬롯 2개 생성 시도
```

### 3️⃣ 취소 시 카운트 불일치

동시에 취소 요청이 들어오면:

* reservedCount가 중복 감소
    
* 음수로 떨어질 가능성
    

👉 **결론**  
이 시스템의 동시성 문제는

> “같은 시간 슬롯을 기준으로 한 경쟁 조건”  
> 에서 발생한다.

---

## 2\. 왜 이 문제는 애플리케이션 락이나 낙관적 락으로 해결하기 어려웠는가?

### ❌ 애플리케이션 락의 한계

* 서버가 여러 대면 보장되지 않음
    
* 트랜잭션과 결합이 어려움
    

### ❌ 낙관적 락의 한계

* 충돌 시 예외 발생
    
* 사용자에게 재시도 요구
    
* 예약 도메인 특성상 UX가 나빠짐
    

예약은:

* **정확성 &gt; 성능**
    
* “다시 시도하세요”가 허용되기 어려움
    
    → 그래서 **비관적 락**을 선택했다.
    

---

## 3\. 해결 전략: ReservationSlot 단위 비관적 락

이 시스템에서 핵심 리소스는 `ReservationSlot`이다.

> 같은 슬롯에 대한 요청은  
> **항상 직렬로 처리되어야 한다**

그래서 전략은 다음과 같다.

* 슬롯 row를 `SELECT … FOR UPDATE`로 잠근다
    
* 락을 잡은 상태에서만 정원 증가/감소를 수행한다
    
* 슬롯이 없을 경우에도 **중복 생성이 발생하지 않도록** 처리한다
    

---

## 4\. 예약 생성 시 동시성 해결 흐름

### 4-1. 슬롯 조회 + 락 획득

```plaintext
reservationSlotRepository
    .findByPhotoLabIdAndReservationDateAndReservationTimeForUpdate(
        photoLab.getId(), date, time
    )
```

이 쿼리는 내부적으로 다음과 같다.

```plaintext
SELECT *
FROM reservation_slot
WHERE photo_lab_id = ?
  AND reservation_date = ?
  AND reservation_time = ?
FOR UPDATE;
```

👉 이 순간, **같은 슬롯에 대한 다른 트랜잭션은 대기 상태**가 된다.

---

### 4-2. 슬롯이 없는 경우: 중복 생성 방지

동시 요청이 들어오면 슬롯 생성도 경쟁 상태가 된다.  
이를 위해 DB에 다음 전제를 둔다.

* `(photo_lab_id, reservation_date, reservation_time)` 유니크 제약
    

그리고 코드에서는:

```plaintext
try {
    reservationSlotRepository.save(slot);
} catch (DataIntegrityViolationException ignored) {
    // 다른 트랜잭션이 먼저 생성한 경우
}
```

이후 **반드시 다시 FOR UPDATE로 조회**한다.

```plaintext
return reservationSlotRepository
    .findByPhotoLabIdAndReservationDateAndReservationTimeForUpdate(...)
```

👉 결과적으로:

* 슬롯은 하나만 생성되고
    
* 모든 요청은 동일 슬롯 row에 대해 직렬화된다
    

---

### 4-3. 정원 초과 방지

```plaintext
slot.increaseReservedCountOrThrow();
```

이 로직은:

* 슬롯 row가 락 잡힌 상태에서 실행됨
    
* 동시에 두 요청이 “자리 있음”이라고 판단할 수 없음
    

따라서 **정원 초과는 구조적으로 발생하지 않는다**.

---

## 5\. 예약 취소 시 동시성 해결 흐름

취소도 동일한 문제가 발생할 수 있기 때문에 락을 사용한다.

### 5-1. 예약 자체를 잠금

```plaintext
reservationRepository
    .findByIdAndPhotoLabIdAndUserIdForUpdate(...)
```

* 중복 취소 방지
    
* 상태 변경 경쟁 방지
    

### 5-2. 슬롯 잠금 후 카운트 감소

```plaintext
ReservationSlot lockedSlot =
    reservationSlotRepository.findByIdForUpdate(slotId);

lockedSlot.decreaseReservedCountSafely();
```

👉 취소 역시 **슬롯 기준으로 직렬 처리**된다.

---

## 6\. 이 구조가 안전한 이유 요약

* 슬롯 단위로 경쟁 범위를 최소화
    
* DB 트랜잭션 + 비관적 락으로 서버 수와 무관하게 일관성 보장
    
* 재시도 로직 없이도 정합성 유지
    
* 예약/취소 흐름 모두 동일한 락 기준 사용
    

---

## 7\. 정리

이 예약 시스템의 동시성 문제는  
“같은 시간 슬롯을 동시에 수정하는 경쟁 조건”에서 발생했다.

이를 해결하기 위해:

* `ReservationSlot`을 동시성의 기준 단위로 삼고
    
* 비관적 락을 통해 요청을 직렬화했다
    

그 결과:

* 정원 초과
    
* 슬롯 중복 생성
    
* 취소 시 카운트 불일치
    

를 모두 구조적으로 방지할 수 있었다.

---

### 한 줄 결론

> 이 예약 시스템에서는 성능보다 정합성이 중요했기 때문에, 슬롯 단위 비관적 락을 선택해 동시성 문제를 해결했다.
