ETC/가상 면접 사례로 배우는 대규모 시스템 설계 기초 2

7장 호텔 예약 시스템

recent0 2025. 7. 29. 22:42

1. 문제 이해 및 설계 범위 확정


1.1 기능 요구사항

  • 5000개 호텔에 100만 개 객실을 갖춘 웹사이트
  • 예약시 결제하는 서비스
  • 10% 초과 예약 가능
  • 객실 가격은 유동적 
  • 서비스 주요 기능
    • 호텔 정보 페이지 표시
    • 객실 정보 페이지 표시
    • 객실 예약 지원
    • 초과 예약 지원

1.2 비기능 요구사항

  • 높은 수준의 동시성 : 성수기이거나 이벤트를 할때 특정 객실에 대해 고객이 많이 몰릴 수 있기 때문
  • 적절한 지연시간 : 예약을 할때 너무 오래만 안 기다릴 정도로 유지

1.3 규모 추정

예약 건수 추정

  • 총 5000개 호텔, 100만개 객실이 있다고 가정
  • 평균적으로 객실의 70%가 사용중이고, 평균 투숙 기간을 3일이라고 가정했을때
  • 일일 예약 건수는 1백만 * 0.7 / 3  = 233,333 (약 240,000)
  • 초당 예약 건수는 3에 근접해 TPS는 3으로 추청

QPS 추정

  • 각 단계마다 이용자의 90%가 이탈한다고 가정
  • 예약 상세 페이지의 QPS는 30, 객실 상세 페이지는 QPS는 300이라고 가정

 

2. 개략적 설계안 제시


2.1 데이터 모델

호텔 예약 시스템 흐름

  1. 호텔 상세 정보 확인
  2. 지정된 날짜 범위에 사용 가능한 객실 유형 확인
  3. 예약 정보 기록
  4. 예약 내역 또는 과거 예약 이력 정보 조회

DB 선택

  • 주요 기능들을 살펴봤을때 쓰기 연산보단 읽기 빈도가 더 많기에 읽기 쓰기에 더 적합한 관계형 DB를 선택하는것이 좋음
  • 관계형 DB를 선택할땐 ACID 속성이 지원되는 DB 사용을 권장. 어플리케이션 코드가 단순해지고 이해하기 쉽기 때문
    • ACID 속성이 만족되지 않으면 잔액 마이너스, 이중 청구, 이중 예약 문제와 같은 문제들이 발생
    • Consistency가 무너지면, 잔액은 0보다 커야한다고 한다는 제약 조건이 지켜지지 않는 문제가 발생
    • Isolation이 지켜지지 않는다면 다른 트랜잭션에서 일어나는 중간 결과를 기반으로 처리해서 문제가 발생
  • 관계형 데이터베이스를 사용하면 스키마 설계도 단순해짐

 

2.2 개략적 설계안 

  • 본 설계안은 마이크로서비스로 설계

3. 상세 설계


3.1 동시성 문제

예약 시스템에서 흔히 발생할 수 있는 동시성 문제들을 나열

  • 같은 사용자가 예약 버튼을 여러 번 누를 수 있다.
  • 여러 사용자가 같은 객실을 동시에 예약하려 할 수 있다.

첫 번째 시나리오:  같은 사용자가 예약 버튼을 여러 번 누를 수 있다.

클라이언트 측 구현

  • 클라이언트 요청을 전송하고 난 다음에 예약 버튼을 비활성화하는 방법
  • 대부분 해결할 수 는 있으나, 자바스크립트를 비활성화하는 경우 이를 우회할 수 있기에 안정적인 방법은 아님

멱등 API 구현

  • 예약 API 요청에 멱등 키를 추가하는 방안
  • 몇번을 호출해도 같은 결과를 내는 API를 멱등 API라고 부름

멱등 API 흐름 예시

사용자가 예약 주문을 생성하기 위해 서버에 요청

                   ↓

서버가 예약 주문서 데이터와 멱등키를 함께 클라이언트에 전달

                   ↓

사용자가 예약 주문서를 작성하고 제출하면 멱등키와 함께 서버에 전달

                   ↓

이때 2번 전달하더라도 멱등키를 기본키로 사용하면 제약 조건에 걸려 새로운 레코드 생성 방지

 

두 번째 시나리오:  여러 사용자가 하나의 객실을 동시에 예약하는 경우

데이터베이스 트랜잭션 격리 수준은 Serializable로 설정되어 있지 않다고 가정

 

방안 1. 비관적 락

  • SELECT FOR UPDATE 문을 사용해서 문제를 해결
  • 간단한 구현, 변경이 끝난 데이터를 갱신하지 않을 수 있다는 장점이 존재
  • 여러 레코드에 락을 걸어 데드락이 발생하는 경우가 발생할 수 있고, 락을 오랫동안 가지고 있어 성능 이슈가 단점

방안 2. 낙관적 락

  • 버전 번호, 타임 스탬프를 이용해서 문제를 해결
  • 데이터베이스에 락을 사용하지 않음, 데이터 일관성 유지라는 장점
  • 데이터에 대한 경쟁이 심하다면 성능이 좋지 못하고 UX까지 나빠질 수 있음. 다음 재시도에서도 성공한다는 보장이 없기 때문

방안 3. 데이터베이스 제약 조건

  • 낙관적 락이랑 비슷한 맥락
  • 잔여 방의 개수가 음수로 갈 수 없다는 제약 조건을 통해 이를 해결할 수 있음
  • 다만 이것도 결국 낙관적 락처럼 제약 조건에 걸려 실패하는 경우 UX가 나빠짐

3.2 시스템 규모 확장

  • 호텔 예약 시스템이 다른 유명 여행 예약 웹사이트와 연동되는 경우, QPS가 기존 대비 몇백 몇천 배 늘어날 수 있음
  • 여기서 시스템은 무상태 서비스이기에 서버를 늘리는 것으로 성능 문제를 해결할 수 있음
  • 데이터베이스는 모든 상태 정보를 저장하기에 DB 서버를 늘리는 것만으로 무리일 수 있음

데이터베이스 샤딩

  • hotel_id를 베이스로 샤딩을 한다면 QPS가 낮아져 데이터베이스 부하가 줄어들고 성능적 이점이 있음

캐시

  • 캐시를 사용해서 호텔 잔여 객실 데이터를 노출 시켜 데이터베이스 부하를 줄일 수 있음
  • 본 데이터는 데이터베이스에 있기 때문에 데이터베이스의 데이터를 활용해 추가 검증이 필요함

캐시를 이용한 설계

아래와 같이 3가지 컴포넌트로 나뉘어 사용한다고 가정

 

예약 서비스

  • 지정된 호텔과 객실 유형, 주어진 날짜 범위에 이용 가능한 객실의 수 질의
  • 객실을 예약 하고 total_reserved의 값 1 증가
  • 고객이 예약을 취소하면 잔여 객실수 다시 증가

잔여 객실 캐시

  • 키 hotelId, roomTypeId, 날짜로 구성
  • 값은 주어진 호텔 ID, 객실 유형 Id, 그리고 날짜에 맞는 잔여 객실 수 

잔여 객실 데이터베이스

  • 잔여 객실 수에 대한 가장 신뢰성 있는 데이터가 보관된 장소

캐시가 주는 과제

캐시 계층이 추가되면서 데이터베이스와 캐시 사이의 데이터 일관성 유지를 고려해야 한다. 사용자가 객실을 예약할 때 아무 문제가 없는 경우 다음 두 가지 작업이 이루어진다

  1. 잔여 객실 수를 캐시로부터 획득
  2. 예약을 한 경우 DB에 바로 반영하고 캐시에 남은 객실 수를 비동기로 적용

CDC 메커니즘

  • 데이터베이스에서 발생한 변화를 감지하여 해당 변경 내역을 다른 시스템에 적용하는 메커니즘
  • Debezium이 하나의 예시, RedHat에서 지원하는 Apache Kafka 기반의 분산 CDC 플랫폼이고 Kafka connect의 소스 커넥터
    • DB의 변경사항을 캡쳐하여 Kafka의 Topic에 변경사항을 발행함

비동기로 하는 경우 문제?

  • 사용자가 남은 객실을 보고 바로 예약을 시도하는 경우가 있을 것
  • 비동기로 인해 캐시 데이터 갱신이 되지 않아 예약을 시도하지만 유효성 검사에서 걸려 예약에 실패함
  • 사용자는 객실 결제에 실패했다기 보단 그 사이에 누군가 예약했구나라고 생각할 수 있기에 UX가 크게 떨어지지 않음

서비스 간 데이터 일관성

모놀리식 서비스

  • 모놀리식 아키텍처는 데이터 일관성을 위해 관계형 데이터베이스를 공유함
  • 잔여 객실 현황 관리, 객실 예약을 하나의 트랜잭션에서 모두 수행하기 때문에 동시성 문제를 효과적으로 해결

 

마이크로 서비스

  • 잔여 객실 데이터베이스, 예약 데이터베이스 등 여러 개의 데이터베이스를 사용한다면 데이터 일관성 문제를 해결하기 어려워짐
  • 만약 잔여 객실 현황 관리 트랜잭션은 성공했지만,  객실 예약 트랜잭션은 실패한 경우 데이터 불일치가 발생하게 됨
  • 이를 해결하기 위한 해결책은 보통 2PC, Saga를 많이 다룸