Skip to main content

비관적 락과 낙관적 락, 그리고 예약 시스템에서의 동시성 문제 해결 전략

Published 3 min readMarkdown

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


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

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

  • 같은 사진관

  • 같은 날짜

  • 같은 시간대

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

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

1️⃣ 정원 초과 문제

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

2️⃣ 슬롯 중복 생성 문제

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

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

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

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

  • reservedCount가 중복 감소

  • 음수로 떨어질 가능성

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

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


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

❌ 애플리케이션 락의 한계

  • 서버가 여러 대면 보장되지 않음

  • 트랜잭션과 결합이 어려움

❌ 낙관적 락의 한계

  • 충돌 시 예외 발생

  • 사용자에게 재시도 요구

  • 예약 도메인 특성상 UX가 나빠짐

예약은:

  • 정확성 > 성능

  • “다시 시도하세요”가 허용되기 어려움

    → 그래서 비관적 락을 선택했다.


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

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

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

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

  • 슬롯 row를 SELECT … FOR UPDATE로 잠근다

  • 락을 잡은 상태에서만 정원 증가/감소를 수행한다

  • 슬롯이 없을 경우에도 중복 생성이 발생하지 않도록 처리한다


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

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

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

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

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) 유니크 제약

그리고 코드에서는:

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

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

return reservationSlotRepository
    .findByPhotoLabIdAndReservationDateAndReservationTimeForUpdate(...)

👉 결과적으로:

  • 슬롯은 하나만 생성되고

  • 모든 요청은 동일 슬롯 row에 대해 직렬화된다


4-3. 정원 초과 방지

slot.increaseReservedCountOrThrow();

이 로직은:

  • 슬롯 row가 락 잡힌 상태에서 실행됨

  • 동시에 두 요청이 “자리 있음”이라고 판단할 수 없음

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


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

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

5-1. 예약 자체를 잠금

reservationRepository
    .findByIdAndPhotoLabIdAndUserIdForUpdate(...)
  • 중복 취소 방지

  • 상태 변경 경쟁 방지

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

ReservationSlot lockedSlot =
    reservationSlotRepository.findByIdForUpdate(slotId);

lockedSlot.decreaseReservedCountSafely();

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


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

  • 슬롯 단위로 경쟁 범위를 최소화

  • DB 트랜잭션 + 비관적 락으로 서버 수와 무관하게 일관성 보장

  • 재시도 로직 없이도 정합성 유지

  • 예약/취소 흐름 모두 동일한 락 기준 사용


7. 정리

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

이를 해결하기 위해:

  • ReservationSlot을 동시성의 기준 단위로 삼고

  • 비관적 락을 통해 요청을 직렬화했다

그 결과:

  • 정원 초과

  • 슬롯 중복 생성

  • 취소 시 카운트 불일치

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


한 줄 결론

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

No comments yet. Be the first to comment.

More from this blog