Skip to main content

Command Palette

Search for a command to run...

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

Updated
3 min read

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을 동시성의 기준 단위로 삼고

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

그 결과:

  • 정원 초과

  • 슬롯 중복 생성

  • 취소 시 카운트 불일치

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


한 줄 결론

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

More from this blog

[Elasticsearch] Elasticsearch 기본용어와 CRUD 명령어

-elastic ▶ 들어가며 이번 글에서는 Elasticsearch를 공부하면서 가장 먼저 익혀야 하는 기본 용어를 정리하고,직접 코드를 쳐가며 CRUD(Create / Read / Update / Delete) 명령어에 익숙해지는 시간을 가져보려고 한다. Elasticsearch는 처음 보면 생소한 용어가 많아서 막막할 수 있는데,사실 구조적으로는 우리가 익숙한 MySQL과 닮은 부분이 굉장히 많다. 둘 다 데이터베이스라는 큰 틀 안에서 데...

Feb 17, 20264 min read

Jpa N+1 문제, 우리는 이렇게 잡았다 — 1:1 문의 Api 실전 최적화기

코드 리뷰 한 줄에서 시작된 쿼리 최적화 여정 1. 시작 — "일단 돌아가게 만들자" Finders 프로젝트에서 1:1 문의(Inquiry) API를 맡았다. 현상소에 문의를 남기고, 답변을 받고, 목록을 조회하는 — 평범한 CRUD다. "JPA 쓰면 쿼리 안 짜도 되는 거 아니야?" 솔직히 처음엔 그렇게 생각했다. JpaRepository에 findAll, findById 쓰면 끝이니까. // 첫 번째 버전의 목록 조회 (QueryDS...

Feb 11, 20268 min read

[모니터링] Sentry 도입부터 Discord 에러 알림까지 — 서버 감시 시스템 구축기

"서버 죽었는데 아무도 몰랐다"에서 "에러나면 1분 안에 안다"까지 1. 모니터링을 시작한 계기 프론트엔드: "API 안 되는데요?"백엔드: "엥? 언제부터요?"프론트엔드: "...2시간 전부터요?"백엔드: 😱 어느 날 서버가 죽어있었는데 아무도 몰랐다. 그날 이후, 모니터링 시스템 구축을 결심했다. (Issue #102) 2. 모니터링 도구 비교: 뭘 쓸까? 처음에는 여러 도구를 비교했다. 도구무료 티어장점단점 Sent...

Feb 11, 20268 min read

[CI/CD] 수동 태그에서 자동 릴리즈까지 — Git Flow와 Auto Release

🚀 우리 auto-release.yml 바로 보러가기 → 이 글에서 설명하는 워크플로우의 전체 코드를 바로 확인할 수 있다! main에 머지만 하면 버전 태그부터 릴리즈 노트까지 알아서 생긴다 1. 우리의 Git 전략: Git Flow (경량 버전) Finders 프로젝트는 Git Flow 전략을 사용하고 있다. 다만 hotfix나 release 브랜치 없이, 조금 가볍게 운영한다. main ← 운영 서버 (prod) 배포 브...

Feb 11, 20265 min read

[ci/cd] 대학생 팀의 배포 파이프라인 진화기

PR 하나면 끝나는 무중단 배포까지, 삽질의 기록 1. 시작은 단순했다 "서버에 올려야 하는데... 어떻게 하지?" Finders 프로젝트를 시작했을 때, 배포라는 걸 해본 적이 없었다. 그래서 처음에는 이렇게 했다. 1. SSH로 서버 접속 2. git pull 3. ./gradlew build 4. java -jar app.jar 당연히 문제가 생겼다. 빌드하는 동안 서버가 꺼져있음 (프론트: "API 왜 안 돼요?" 🔥) 빌...

Feb 11, 20267 min read
F

Finders Tech Blog

16 posts