CS/기타

[대규모 시스템 설계 기초 2] 7장. 호텔 예약 시스템 파헤치기

Joonfluence 2024. 4. 23.

이론

들어가기 앞서

오늘은 가상면접사례로 배우는 대규모 시스템 설계 기초 2의 7장 호텔 예약 시스템의 내용을 읽고 정리해봤습니다. 호텔 예약 시스템은 해당 내용을 바탕으로 회사 강의 결제 시스템 에 녹여낼 수 있는 부분이 많아보였습니다. 또한 사내 운영 개선 업무를 하면서 적용했던 부분과 연관되는 부분들이 많아 흥미로웠습니다.

요구사항 분석

  1. 5000개의 호텔에 100만 개 객실을 갖춘 호텔 체인을 위한 웹사이트를 구축하라.
  2. 결제는 예약 시, 전부 진행한다.
  3. 결제는 호텔 웹사이트 혹은 앱에서만 가능하다.
  4. 10% 초과 예약이 가능해야 한다.
  5. 객실 가격은 그날 상황에 따라 유동적이어야 한다.

비기능 요구사항

  1. 높은 수준의 동시성 지원 : 성수기, 대규모 이벤트 기간에 고객이 많이 몰릴 때, 동시성 이슈를 예방할 수 있어야 한다. 여기서 동시성 이슈란 공유자원에 여러 클라이언트가 동시에 접근 가능하여, 데이터 정합성이 맞지 않게 되는 문제를 말한다. 호텔 예약 시스템에서는 이중 예약 문제가 발생될 수 있다.
  2. 호텔 예약 시스템이 해당 호텔 웹사이트에만 연동 되는 것이 아니라, booking.com 이나 expedia.com 같은 유명한 여행 예약 웹사이트와 연동되어야 한다. 시스템 부하가 높을 때 병목이 될 수 있는 지점을 파악해라.

동시성 관련

이중 예약 방지

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

같은 사용자가 예약 버튼을 여러 번 누르는 경우

  1. 해결방안 1) 클라이언트 측에서 한번 누르면 버튼을 비활성화한다. 손쉽게 구현이 가능하지만 클라이언트를 우회할 수 있기 때문에 안정적인 방법은 아니다.
  2. 해결방안 2) 멱등한 API를 만든다. 멱등하다란 것은 여러 번 호출해도 동일한 결과를 보장한다란 의미이다. 이를 위해, 예약 테이블의 여러 row에 대한 멀티 유니크키 제약을 걸 수 있다. reservation_id, start_date, end_date, status, guest_id 등 이 값들의 조합은 중복될 수 없기 때문.

하나 남은 잔여 객실을 여러 유저가 동시에 예약한 경우

  • 락(lock)을 활용하여, 공유 자원에 동시에 접근하지 못하도록 막거나, 요청이 들어온 순서대로 처리되도록 별도의 큐를 사용할 수도 있다. 여기선 락을 활용한 방법에 대해 다룬다.
  1. 해결방안 1) 비관적 락을 적용한다.
    • 장점 : 구현이 쉽다. 데이터 경합이 심할 때 유용하다.
    • 단점 : 교착 상태(데드락)이 발생할 수 있다. 확장성이 낮다. 트랜잭션은 락이 걸린 자원에 접근할 수 없기 때문 (상호배제)
  2. 해결방안 2) 낙관적 락을 적용한다. 버젼 번호와 타임스탬프의 두 가지 방법으로 구현 가능하다.
    • 장점 : 락을 걸지 않아도 된다.
    • 단점 : 데이터에 대한 경쟁이 치열한 상황에서는 성능이 좋지 못하다. 모든 요청에 대하여 접근을 허용하기 때문.
  3. 해결방안 3) 데이터베이스 제약 조건을 활용한다. CONSTRAINT check_room_count CHECK ((total_inventory - total_reserved >= 0)) 와 같이
    • 장점 : 구현이 쉽다.
    • 단점 : 낙관적 락과 마찬가지로 안좋은 사용자 경험을 줄 수 있다. 분명 잔여 객실이 있다고 확인했는데, 막상 결제할 때는 "객실이 없습니다"라는 응답을 받기 때문.

시스템 부하 개선 관련

성능 개선 방법

  1. 데이터베이스 샤딩 : 샤딩을 적용함으로써, 데이터베이스에 몰리는 부하를 분산한다. 샤딩이란 동일한 스키마를 가지고 있는 데이터를 다수의 데이터베이스에 분산하여 저장하는 기법이다. 수평 분할 (Horizontal Partitioning) 과 유사하지만 차이가 있는데, 수평 분할은 동일한 스키마를 가진 데이터를 다수의 테이블에 분산하여 저장하는 기법으로, 동일한 서버에 저장한다는 차이가 있다. 호텔 예약 시스템에서는 샤딩 조건으로 질의에 주로 사용되는 hotel_id를 쓸 수 있다. 한 대의 MySQL 서버가 감당할 수 있는 부하는 2000 QPS 정도로, 만약 시스템의 QPS 30000이면, 적정한 샤드의 수는 16대 (30000/16 = 1875 QPS)이다. 분할 기법에는 여러가지가 있지만, 대표적으로 Hash Partitioning이 있고 이는 분할 키 값의 범위를 샤드의 수로 남은 나머지로 분할하는 것이다. hotel_id % 16 = 데이터가 저장될 샤드 번호가 된다.

  2. 캐싱 : 잔여 객실 확인 작업은 쓰기 연산보다 읽기 연산이 횔씬 많다. 따라서 호텔 잔여 객실 데이터를 캐싱한다. 그러면 대부분의 읽기 연산을 캐시가 처리함으로써, DB 부하 및 응답 속도를 줄일 수 있다. 호텔 잔여 객실 데이터는 과거 데이터가 중요하지 않다. 따라서 낡은 데이터는 자동적으로 소멸되도록 TTL을 설정하는 것이 좋다. 레디스는 이러한 상황에 적합한데 TTL과 LRU 캐시 교체 정책을 사용하여 메모리를 최적으로 활용할 수 있다. 사전에 잔여 객실 정보를 캐시에 미리 저장해둔다. 키는 hotel_ID_RoomType_ID_{날짜}가 된다.

  • 장점
    • DB 부하가 크게 준다.
    • 메모리에서 처리되므로 읽기 질의 속도가 빠르다.
  • 단점
    • DB와 캐시 사이의 데이터 일관성을 유지하는 것이 어렵다. 이를 위해 최종적으로 데이터베이스에 잔여 객실 확인을 하도록 하여 캐시 질의 결과와 데이터베이스 질의 결과의 차이가 없도록 한다.

서비스 간 데이터 일관성 유지 문제

  • 본 설계는 MSA 기반으로 구축된 것을 가정한다. 호텔 서비스, 요금 서비스, 예약 서비스, 결제 서비스가 각각 별도의 서버로 분리되어 있다. 각각의 데이터베이스 역시 분리된다. 이 때 서비스 간 데이터 일관성을 유지하는 방법에는 2가지가 있다.

해결방법

  • 2단계 커밋 : 모든 노드가 성공하든 아니면 실패하든 둘 중 하나로 트랜잭션이 마무리되도록 보증한다는 것이다. 2PC는 비중단 실행이 가능한 프로토콜이 아니므로 어느 한 노드에 장애가 발생하면 해당 장애가 복구될 때까지 진행이 중단된다.
  • 사가 : 각각의 트랜잭션이 완료되면 다음 트랜잭션을 시작하는 트리거로 쓰일 메세지를 만들어 보낸다. 어느 한 트랜잭션이라도 실패하면 사가는 그 이전 트랜잭션의 결과를 전부 되돌리는 트랜잭션을 순차적으로 실행한다. 2PC는 여러 노드에 걸친 하나의 트랜잭션을 통해 ACID 속성을 만족시키는 개념이지만 사가는 각 단계가 하나의 트랜잭션이라서 결과적 일관성에 의존한다.

적용

다음 번에는 실제로 코드 레벨에서 적용된 내용을 설명 드리겠습니다.

반응형

댓글