비관적 락과 낙관적 락, 그리고 예약 시스템에서의 동시성 문제 해결 전략
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을 동시성의 기준 단위로 삼고비관적 락을 통해 요청을 직렬화했다
그 결과:
정원 초과
슬롯 중복 생성
취소 시 카운트 불일치
를 모두 구조적으로 방지할 수 있었다.
한 줄 결론
이 예약 시스템에서는 성능보다 정합성이 중요했기 때문에, 슬롯 단위 비관적 락을 선택해 동시성 문제를 해결했다.