CS/기타

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

Joonfluence 2024. 4. 23. 00:54

이론

들어가기 앞서

오늘은 가상면접사례로 배우는 대규모 시스템 설계 기초 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 속성을 만족시키는 개념이지만 사가는 각 단계가 하나의 트랜잭션이라서 결과적 일관성에 의존한다.

적용

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

반응형