Chapter 7

호텔 예약 시스템

  • 7.1 문제 이해 및 설계 범위 확정
  • 7.2 개략적 설계안 제시
  • 7.3 상세 설계
  • 7.4 마무리

호텔 예약 시스템은 에어비앤비나 부킹닷컴 같은 서비스에서 객실을 예약하는 시스템이야. 핵심 난이도는 동시성 제어 — 두 사람이 동시에 같은 객실을 예약하려 할 때 이중 예약(double booking)을 어떻게 막을 것인가.

기능 요구사항을 보면:

  • 호텔/객실 정보를 조회한다
  • 특정 날짜에 객실을 예약한다
  • 예약을 취소한다
  • 호텔 관리자가 객실과 가격 정보를 관리한다

비기능 요구사항은:

  • 이중 예약 방지 — 같은 객실이 같은 날짜에 두 번 예약되면 안 돼
  • 높은 동시성 지원 — 인기 호텔은 수천 명이 동시에 예약을 시도할 수 있지
  • 적절한 지연시간 — 예약 요청에 대한 응답이 수 초 이내여야 해

규모를 잡아보면 — 호텔 5,000개, 객실 총 100만 개, 일일 예약 건수 약 100만 건. QPS로 치면 초당 약 12건이라 높지 않지만, 인기 호텔에 동시 요청이 몰리는 게 문제야.

주요 API는 간단해.

  • GET /hotels/{id} — 호텔 상세 정보
  • GET /hotels/{id}/rooms — 객실 목록과 가용성
  • POST /reservations — 예약 생성
  • DELETE /reservations/{id} — 예약 취소

핵심 테이블은 세 개야.

hotel — 호텔 정보 (이름, 주소 등)

room_type — 객실 유형별 정보 (디럭스, 스탠다드 등, 총 객실 수)

reservation — 예약 기록 (호텔 ID, 객실 유형, 체크인/체크아웃, 사용자 등)

가용성 확인을 위해 room_type_inventory 테이블이 있어. (hotel_id, room_type_id, date, total_inventory, total_reserved) 형태지. 특정 호텔의 특정 객실 유형이 특정 날짜에 얼마나 남았는지를 관리해.

예약 흐름은 이래:

  1. 사용자가 객실 유형과 날짜를 선택해서 예약 요청
  2. room_type_inventory에서 해당 날짜 범위의 잔여 객실을 확인
  3. 잔여가 있으면 total_reserved를 +1 하고 예약 레코드를 생성
  4. 잔여가 없으면 "만실" 반환

2번과 3번 사이에 문제가 생기지. 두 사용자가 동시에 잔여를 확인하면 둘 다 "1개 남음"을 보고, 둘 다 예약을 시도해. 결과적으로 2개가 예약돼서 오버부킹.

이걸 해결하는 방법이 여러 가지야.

방법 1: 비관적 잠금(Pessimistic Locking)

SELECT ... FOR UPDATE 같은 쿼리로 해당 행을 읽을 때 락을 걸어. 다른 트랜잭션은 이 행을 읽거나 수정할 수 없고, 대기해야 하지.

장점은 확실하게 이중 예약을 방지해. 단점은 락이 걸려 있는 동안 다른 요청이 대기하니까 처리량이 떨어져. 인기 호텔에 수천 명이 동시에 몰리면 대기 시간이 길어지지. 데드락 가능성도 있고.

방법 2: 낙관적 잠금(Optimistic Locking)

락을 걸지 않고 읽어. 대신 업데이트할 때 버전 번호를 확인하지. "내가 읽었을 때 version=5였으니, version=5인 경우에만 업데이트해줘" 하고 조건부 업데이트.

UPDATE room_type_inventory
SET total_reserved = total_reserved + 1, version = version + 1
WHERE hotel_id = ? AND room_type_id = ? AND date = ?
  AND version = 5

version이 이미 6으로 바뀌어 있으면 업데이트가 0건이 돼서 실패를 감지할 수 있어. 실패하면 다시 읽고 재시도.

장점은 락이 없으니 동시 읽기 성능이 좋아. 단점은 충돌이 자주 발생하면 재시도가 반복돼서 오히려 비효율적이지.

방법 3: 제약 조건 기반

DB에 유니크 제약 조건을 걸어서 이중 예약 자체를 불가능하게 만들어. 예약 테이블에 (hotel_id, room_type_id, date) 조합이 특정 수를 초과하면 제약 위반으로 실패.

하지만 이 방식은 구현이 복잡하고, "남은 수량"이라는 개념을 제약 조건으로 표현하기가 쉽지 않지.

책에서 추천하는 건 낙관적 잠금과 조건부 업데이트의 조합이야. 이렇게:

UPDATE room_type_inventory
SET total_reserved = total_reserved + 1
WHERE hotel_id = ? AND room_type_id = ? AND date = ?
  AND total_reserved + 1 <= total_inventory

핵심은 total_reserved + 1 <= total_inventory 조건이야. 이 조건 덕분에 동시에 여러 트랜잭션이 실행돼도, 재고가 0이 되면 더 이상 예약이 안 돼. 별도의 버전 번호도 필요 없지.

이 방법은 호텔 예약처럼 충돌이 상대적으로 적은 시나리오에 적합해. 같은 호텔, 같은 객실 유형, 같은 날짜에 동시에 예약하는 건 그렇게 흔한 일이 아니니까.

네트워크 문제로 사용자가 예약 버튼을 두 번 누르거나, 클라이언트가 재시도를 하면 같은 예약이 두 번 생길 수 있어. 이를 막기 위해 **멱등성 키(idempotency key)**를 사용하지.

클라이언트가 예약 요청 시 고유한 멱등성 키를 포함하고, 서버는 이 키가 이미 처리됐는지 확인해. 처리됐으면 기존 결과를 반환하고, 새 요청이면 정상 처리.

예약에는 상태 머신이 필요해.

pendingconfirmedchecked_inchecked_out pendingcancelled confirmedcancelled

pending 상태는 타임아웃이 필요하지. 결제를 안 하고 방치하는 사용자가 있으면 일정 시간 후 자동으로 cancelled로 전환해서 재고를 풀어줘야 해.

전체 QPS는 낮지만, 읽기(가용성 조회)가 쓰기보다 훨씬 많아. 읽기 복제본을 두면 읽기 부하를 분산할 수 있지. 캐시(Redis)도 활용 가능한데, 재고 데이터를 캐싱할 때 캐시와 DB의 일관성에 주의해야 해.

더 큰 규모에서는 호텔 ID 기준으로 데이터베이스 샤딩을 할 수 있지.

호텔 예약 시스템은 "동시성 제어"를 연습하기 딱 좋은 문제야. 비관적/낙관적 잠금의 트레이드오프를 이해하고, 실제로 어떤 상황에 어떤 방법이 적합한지를 판단하는 게 면접에서의 핵심 포인트지.


정리

7장 읽고 기억할 거 세 가지:

  1. 이중 예약 방지의 핵심은 DB 레벨의 조건부 업데이트야. total_reserved + 1 <= total_inventory 같은 WHERE 조건으로 원자적으로 재고를 확인하고 차감하지.
  2. 비관적 잠금은 확실하지만 느리고, 낙관적 잠금은 빠르지만 충돌 시 재시도가 필요해. 충돌 빈도에 따라 적합한 방법이 달라지지.
  3. 멱등성 키로 중복 예약을 방지하고, 타임아웃으로 방치된 예약을 정리해. 실전에서 놓치기 쉬운 엣지 케이스들이야.