[대규모 시스템 설계 기초 2] 호텔 예약 시스템

Jake·2026년 5월 2일

호텔 예약 시스템을 설계하기 위해 우선 기능적 요구사항과 비기능적 요구사항을 파악한다. 요구사항을 파악할때는 고객의 워크플로우를 따라가며 각 항목들에 대해 어떤것들이 기능적, 비기능적 요구사항인지 분석한다.


기능적 요구사항

  • 시스템 규모: 5000개 호텔, 100만개 객실
  • 대금 지불은 예약시 지불
  • 객실 구매는 웹사이트에서만 허용
  • 취소도 가능해야함
  • 10% 초과예약 가능해야함
  • 가격 변동이 유동적으로 되어야 함 주기는 ? 1일, 1주일 다양하다

비기능적 요구사항

  • 높은수준의 동시성 지원
    • 성수기나 대규모 이벤트 기간에는 일부 인기 호텔의 특정 객실예약이 폭증하므로
  • 적절한 지연시간
    • 예약시 빠르면 좋지만 예약 처리때 몇초는 괜찮음

규모추정

현재까지의 요구사항들에 대해 트래픽을 추정해보자.

상황을 아래처럼 가정한다

  • 70% 객실이 예약
  • 3일 투숙

100만개 객실이므로 (100만 * 0.7) / 3 = 대략 24만건 그리고 이걸 초단위로 환산하면 초당 시스템을 통한 예약은 3건이다. TPS = 3

각 페이지별 QPS(Query per second) 를 구해보자. 그리고 각 페이지별 비용 특징에 대해 알아본다.

  • 호텔/객실 상세페이지: 호텔/객실 정보를 확인 (조회)
  • 예약 상세정보: 날짜, 투숙인원, 결제 방법 (조회)
  • 객실 예약 페이지: 예약 버튼을 통해 예약 (트랜잭션)

사용자들은 각 단계에 대해 다음단계로 10%만 이동하며 90%는 이탈한다고 가정한다.

내용QPS
객실 상세300
예약 상세30
예약3

이 표는 최종 예약이 TPS = 3 이므로 이를 역산한 과정이다.


설계안 제시

위 요구사항을 바탕으로 API 설계를 한다.

호텔 API

HTTP methodEndpoint상세
GET/v1/hotels/id호텔 상세 정보
POST/v1/hotels신규 호텔 생성
PUT/v1/hotels/id호텔 정보 갱신
DELETE/v1/hotels/id호텔 정보 제거

객실 API

HTTP methodEndpoint상세
GET/v1/hotels/:id/rooms/id객실 상세 정보
POST/v1/hotels/:id/rooms신규 객실 생성
PUT/v1/hotels/:id/rooms/id객실 정보 갱신
DELETE/v1/hotels/:id/rooms/id객실 정보 제거

예약 API

HTTP methodEndpoint상세
GET/v1/reservations/id예약 상세 정보
POST/v1/reservations신규 예약 생성
PUT/v1/reservations/id예약 정보 갱신
DELETE/v1/reservations/id예약 정보 제거

예약시 반드시 필요한 인자는 무엇일까 ? startDate, endDate, reservationId, roomId 등이 있다. 이때 reservationId 는 이중예약 방지를 위한 멱등키로 활용될 수 있다.


데이터 모델

데이터베이스 선택 전 데이터 접근 패턴을 살펴보면 데이터는 주로 호텔 상세 정보, 지정한 날짜의 사용 가능한 객실 정보, 사용자가 예약한 예약 정보 등이 있다.

이 조회 패턴에는 관계형 데이터베이스가 적절하다. 그 이유는

  • 사용자는 예약(쓰기 연산) 보다 객실 상태 조회 (조회 연산) 을 압도적으로 많이 사용하는데 관계형 데이터베이스는 이 조회 빈도가 쓰기 연산에 비해 높은 작업 흐름을 지원하도록 설계되어있음
  • NoSQL은 쓰기 연산에 최적화
  • ACID 를 지원하여 원자성, 일관성, 격리성, 영속성 특징들을 통해 예약시 이중청구 방지등이 가능
  • 데이터 베이스 구조를 명확히 할 수 있음

DB 스키마

hotel / room / room_type_rate / reservation / guest
로 나눌 수 있겠다


예약 상태

그리고 reservation 은 상태를 가져야 하며 위와 같이 결재 대기, 취소, 결제완료, 승인실패, 환불완료 를 가진다.


개략적인 설계안

위 설계안은 마이크로 서비스 아키텍처이다

CDN: js 코드 번들, 이미지, 동영상 등 정적 데이터를 캐시하여 웹로드 성능을 개선
API 게이트웨이: 처리율 제한, 인증 등 기능 지원, 엔드포인트 기반 특정 서비스로 요청 전달
결제 서비스: 고객 결제 처리, 절차 성공시 예약 상태 갱신

위 마이크로 서비스에서는 서비스간 통신을 위해 gRPC 를 이용하여 통신하기로 한다.

gRPC 특징으로는 JSON같이 텍스트 기반이 아닌 이진 직렬화를 사용하며 HTTP2를 통해 멀티 플렉싱, 헤더 압축 을 통해 효율적으로 전달이 가능하고 강력한 타입 체크를 지원하여 장점이 많다.

아래는 다른 대안들에 대한 비교 표이다.

비교 항목gRPCRESTGraphQLMessage Queue
직렬화Binary (Protobuf)Text (JSON)Text (JSON)Binary/Text
프로토콜HTTP2HTTP/1.1HTTP/1.1/2자체 프로토콜
통신 방식동기/비동기 (Stream)동기 (Request-Reply)동기비동기(pub/sub)
타입 안정성매우 높음낮음높음보통
성능매우 높음보통보통높음(Throughput)

상세설계

실제 호텔에서는 사실 각 객실별 Id를 가지기 어렵다 보통 예약을 할때 객실 유형을 선택하여 예약한다. 그러므로 테이블 스키마를 아래와 같이 변경한다.

room은 room_type_id 컬럼을 가지도록 하고
reservation 테이블은 reservation_id(멱등키), hotel_id, room_type_id 를 컬럼으로 가진다.
roome_type_inventory 테이블은 예약시 남은 예약 수 자원을 관리하는 중요한 테이블로 hotel_id, room_type_id, date 컬럼을 엮어서 복합키를 기본키로 가지도록 한다. 그리고 total_inventory, total_reserved 컬럼을 통해 전체 예약 객실 수, 예약 된 객실 수 정보를 가진다.
각 방의 타입별로 날짜당 하나의 레코드를 가지게하여 예약가능한 방 수를 관리하면 관리가 쉽다.

용량에 대한 추정은 5000개 호텔 20개 객실 유형 2년 * 365 = 약 7300만개 이다
이정도의 데이터는 1개의 데이터베이스로 가능하지만 하나만 두면 SPOF(단일 실패 포인트) 문제가 가능하므로 여러 지역에 DB를 복제해두는 것이 좋다.

예약시 조회하는 데이터는 아래의 room_type_inventory 테이블이다.

datetotal_inventorytotal_reserved
2026-01-0110097
2026-01-0210087
2026-01-0310077

여기서 10% 초과 예약에 대해 반영하려면 ?

if ((totalReserved + 예약할 방수) <= 1.1 * total_inventory) -> true

가 되어야 한다.

만약 저 데이터가 단일 DB에 담기 너무 큰 양이라면 ? 예약 데이터는 현재 기준으로 현재와 미래의 데이터를 조회하므로 이전 데이터를 조회하고 변경하는 경우는 드물다. 따라서 현재와 향후 데이터만 저장하거나 hotel_id 로 샤딩 전략을 선택하여 여러 데이터베이스에 나누어 데이터를 저장할 수 있다.


동시성

경쟁조건

이중 예약을 어떻게 방지할까 ?

  1. 같은 사용자가 예약 버튼을 여러번 누를 수 있다.
  2. 서로 다른 사용자가 동시에 예약 버튼을 누를 수 있다.

1번 같은 경우 멱등키를 사용하여 멱등 API를 만드는 것이다. 예약시 주문지 생성을 해서 미리 reservation_id를 생성하도록 하고 이 id는 전역적으로 유일하도록 한다. 이 키를 통해 여러번 생성해도 새로운 레코드는 생성되지 않도록 한다.

2번 의 경우 트랜잭션 격리 수준이 serializable 로 설정되어있지 않으면 1번 트랜잭션 커밋 전 2번 트랜잭션은 똑같은 값을 읽게 되어 2중 예약 발생이 가능하다.

이때 락을 사용할 수 있으며 비관적 락과 낙관적 락을 고려할 수 있다.


비관적 락

  • 비관적 락은 레코드에 락을 걸어 먼저 락을 건 사용자가 변경을 마치고 락을 해제할 때 까지 다른 트랜잭션에 대기를 하게 하는 기법이다.
  • 1번이 먼저 락을 걸면 2번은 1번 트랜잭션이 끝나기까지 대기한다.

장점: 구현이 쉽고 락 경합이 심할때 유용하다
단점: 여러 레코드에 락을 걸면 데드락이 발생할 수 있으며, 코드 작성이 까다롭다. 락을 오래 잡고있게 되면 확장성도 떨어지게된다

예약 시스템에서는 비교적 경합이 낮고 (TPS = 3) 확장성이 중요하므로 비관적 락을 권장하지 않는다.


낙관적 락

  • 낙관적 락은 여러 사용자의 동시 갱신을 우선 허용한다. 버전 번호를 통해 유효성 검사를 현재 버전 번호보다 1큰 값인지를 통해 판단하고 이때 실패하면 트랜잭션은 중단되고 구현한 로직을 통해 다시 DB 조회를 통해 현재 버전 번호를 가져와서 로직 수행을 진행한다.
  • 낙관적 락은 DB에 락을 걸지 않아 비관적 락보다 일반적으로 빠르다. 하지만 동시성 수준이 높으면 성능이 급격히 나빠진다. 왜냐면 결국 모든 동시 요청에 대해 성공은 1개만 이루어지므로 나머지 사용자는 모두 다시 재시도를 해야하기 때문이다. 최종 결과는 정확하지만 재시도 횟수가 클라이언트 수만큼 늘어나므로 성능이 나빠진다.

장점: DB에 락을 걸지 않고 버전 번호를 통해 관리하므로 일관성은 애플리케이션에서 관리된다. 락 관리 비용 없이 트랜잭션을 실행할 수 있다
단점: 데이터 경쟁이 치열할때 성능이 나빠진다


시스템 규모 확장

만약 시스템 부하가 급격히 높아진다면 무엇이 병목일까 ? 서버는 수평 확장하고 로드밸런싱을 하면 되지만 DB는 그게 어려울 수 있다.

DB 샤딩

DB 샤딩을 적용하면 된다. DB를 여러대 두고 각각 일부 데이터만 보관한다. 그렇다면 이때 어떤 기준으로 데이터를 분배해야할까 ? DB에 해당 테이블에 대해 조회시 기준이 되는 것을 선택한다. hotel 관련 테이블에서는 예를 들어 hotel_id가 될 수 있다.


캐시

호텔 예약 도메인 특성상 현재, 미래 데이터만 중요하다. 과거 이력을 조회하거나 수정하는 경우는 드물기 때문이다. 그래서 캐시 정책을 TTL, LRU 를 사용한다. 즉 데이터는 자동 소멸되도록 하고 가장 적게 사용한 캐시 데이터를 제거하도록 한다. 다른 캐시 정책으로는 LFU, FIFO 등이 있다.

LRU가 동작하는 방식은 데이터에 접근하면 위치를 가장 앞으로 옮기고 메모리가 차면 가장 뒤에 데이터를 없애는 방식이다. 그래서 단점으로는 이런 작업에 대한 오버헤드가 커질 수 있다는 점이다.

조회 요청이 DB로 직접적으로 가게 되면 부하가 커질 수 있다. 그래서 캐시 레이어를 두고 여기서 잔여 객실 확인 요청이 처리되도록 한다. 갱신은 DB가 처리하도록 하여 최종 진실은 DB가 가지도록 한다. 갱신이 되면 이후 DB는 캐시로 비동기 갱신 처리를 진행하도록 한다.

대부분의 잔여 객실 읽기 연산은 캐시를 통해 처리되도록 한다. 이는 예약 시스템이 쓰기 연산보다 읽기 연산이 훨씬 많아 이를 효율적으로 처리하기 위함이다. 하지만 일관성 문제를 해결해야한다. 이를 위해 CDC 메커니즘을 사용하 수 있다.
이는 DB에 변경된 내용들을 다른 서비스로 전파하는 방식이다. 주로 드베지움을 사용하며 DB 로그를 추출하여 Kafka Connect 위에서 변경 이벤트를 토픽으로 발행하여 사용된다.


일관성

모놀리틱에서는 데이터 일관성을 위해 DB를 공유한다. 하지만 MSA에서는 각 서비스가 독자적으로 DB를 가져야할 수 있다. 이 경우 하나의 원자적 연산에 대해 여러 서비스에 연산들이 여러 DB에 걸쳐서 실행되어야 할 수 있다. 그러면 단순 DB의 트랜잭션으로는 이를 원자적으로 처리하기 불가능하다 (왜냐면 DB가 물리적으로 분리되어있으므로) 각기 다른 DB 연산을 원자적으로 묶을 수 없기때문이다.

그래서 2PC, Saga, Outbox 패턴 등을 사용한다.

2PC는 여러 노드에 하나의 트랜잭션을 적용해서 대기가 발생하여 성능이 비교적 낮다.
Saga 에서는 보상 트랜잭션 로직과 OutBox 패턴을 함께 사용한다. DB 저장과 이벤트 발행을 하나의 로컬 트랜잭션으로 묶을 수 있어서 저장과 이벤트 전송의 일관성을 해결한다. 그래서 이 패턴을 사용하기도 한다.

Github

요구사항 구현을 Claude code 를 사용하여 MSA + DDD + Gradle 멀티모듈 로 구현했다. 서비스간 통신은 동기 비동기 Kafka. 데이터는 Database per Service (MySQL), 가용성 조회는 Redis Read Model로 진행하였다.

호텔 예약 서비스 github

┌──────────────────┐      ┌──────────────────┐
│   hotel-service  │      │    rate-service  │
│  (마스터 CRUD +    │      │   (요금 정책)      │
│   Redis Read     │      │                  │
│     Model)       │      │                  │
└────────┬─────────┘      └────────┬─────────┘
         │  hotel-events · rate-events (Kafka)
         ▼                         ▼
┌───────────────────────────────────────────┐
│           reservation-service              │
│   (예약 수명주기 · 재고 SoT)               │
│   ReservationCreated / Cancelled 발행     │
└────────┬──────────────────────────────────┘
         │ Kafka
         ▼
┌──────────────────┐
│   guest-service  │
│ (투숙객 정보)    │
└──────────────────┘

0개의 댓글