Giter VIP home page Giter VIP logo

hotel_reservation's Introduction

마이크로서비스 아키텍처 기반 대용량 트래픽 호텔 서비스 설계

예상 규모

  • 총 5,000개의 호텔, 100만개의 객실이 있다.

  • 평균적으로 객실의 70%가 사용중이다.

  • 10%의 초과 예약 기능이 있다.

  • 평균 투숙 기간은 3일이다.

  • 일일 예상 예약 건수 :

    • 호텔 객실 수 * 객실 사용률 / 평균 투숙 기간
    • 1,000,000 * 0.7 / 3 = 233,333 ( 대략 240,000 건 )
  • 초당 예약 건수 :

    • 일일 예상 예약 건수 / 하루 86,400 ( 대략 105 초 )
    • 240,000 / 100,000 = 대략 3 TPS
  • QPS(Qeuries-per-second) :

    • 약 90%의 사용자는 다음 단계에 도달하기 전에 흐름을 이탈한다고 가정한다.
      • 호텔/객실 상세 페이지 : 조회 (300)
      • 예약 상세 정보 페이지 : 조회 (30)
      • 객실 예약 페이지 : 예약 (트랜잭션 발생) (3)
QPS

API 설계

  • 호텔 관련 API

    • GET /v1/hotels - 모든 호텔의 정보 반환

    • GET /v1/hotels/id - 호텔의 상세 정보 반환

    • POST /v1/hotels - 신규 호텔 추가. 호텔 직원만 사용 가능

    • PUT /v1/hotels/id - 호텔 정보 갱신. 호텔 직원만 사용 가능

    • DELETE /v1/hotels/id - 호텔 정보 삭제. 호텔 직원만 사용 가능

  • 객실 관련 API

    • GET /v1/hotels/:id/rooms/id - 객실 상세 정보 반환

    • POST /v1/hotels/:id/rooms - 신규 객실 추가. 호텔 직원만 사용 가능

    • PUT /v1/hotels/:id/rooms/id - 객실 정보 갱신. 호텔 직원만 사용 가능

    • DELETE /v1/hotels/:id/rooms/id - 객실 정보 삭제. 호텔 직원만 사용 가능

  • 예약 관련 API

    • GET /v1/reservations - 로그인 사용자의 예약 이력 반환

    • GET /v1/reservations/id - 특정 예약의 상세 정보 반환

    • POST /v1/reservations - 신규 예약

    • DELETE /v1/reservations/id - 예약 취소

  • 회원 관련 API

    • POST /v1/guests - 회원가입

    • POST /v1/auth/login - 로그인

데이터 베이스 모델

호텔 예약 시스템의 경우 호텔을 실제로 예약하는 사용자 수보다 호텔 웹사이트/앱을 방문하는 사용자 수가 압도적으로 많기 때문에 읽기 빈도가 쓰기 연산에 비해 높은 작업의 흐름을 잘 지원하는 관계형 데이터 베이스를 사용한다.

해당 프로젝트에서는 간단한 데이터 베이스 모델을 사용하고 복잡한 쿼리를 사용하진 않을 것이기 때문에 대량의 읽기 작업에 적합한 Mysql을 선택하였다.

이 프로젝트의 핵심은 아키텍처 구성이므로 데이터베이스 모델은 아래와 같이 간단하게 구성하였다.

ERD 설계

reservation 테이블의 status는 각각 결제 대기, 결제 완료, 환불 완료, 취소, 승인 실패의 다섯 상태 가운데 하나의 값을 가진다. 이를 다이어그램으로 표현하면 아래 그림과 같다.

예약 상태

room_type_inventory 테이블의 기본키는 room_type_id, date의 복합키다. 저장 용량을 측정하면 5,000개의 호텔이 있고 각 호텔에는 20개의 객실 유형이 있다고 가정한다. 또한 실제 서비스시 이 테이블에서는 약 2년 이내 모든 미래 날짜에 대한 가용 객실 데이터 질의 결과를 토대로 미리 채워 놓고, 새로 추가해야하는 객실 정보는 매일 한 번씩 일괄 작업을 돌리는 작업이 필요하다.

따라서 레코드의 수는 5,000 _ 20 _ 2년 * 365일 = 7,300만개 정도가 된다. 하나의 데이터 베이스로 저장하기 충분하지만, 데이터 베이스 서버를 하나만 둘 경우 단일 장애점 문제를 피할 수 없다. 따라서 고가용성을 달성하려면 데이터베이스를 복제하여 사용하여야 한다.

동시성 문제

호텔 예약에 경우 아래와 같이 두 가지의 이중 예약 문제를 해결해야 한다.

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

첫 번째 시나리오의 경우 클라이언트에서 요청을 전송하고 난 다음 예약 버튼은 disabled 할 수 있다. 하지만 만약 사용자가 자바스크립트 비활성화하여 클라이언트 측 확인을 우회할 수도 있다.

따라서 API 요청에 idempotent key(멱등키)를 추가하여 몇번을 호출해도 같은 결과를 내는 API를 개발한다. 상세한 절차는 아래와 같다.

sequenceDiagram
    participant 사용자
    participant 예약 서비스
    사용자->>예약 서비스: 1. 예약 주문 생성
    예약 서비스-->>사용자: 2. 예약 주문서 표시 (reservation_id)
    사용자->>예약 서비스: 3. 예약 제출 (reservation_id)
    사용자-x예약 서비스: 4. 예약 제출 (reservation_id)
  1. 예약 주문 생성

    • 고객이 예약 세부 정보를 입력하고 '예약' 버튼을 누르면 예약 서비스는 예약 주문을 생성한다.
  2. 예약 주문서 표시 (reservation_id)

    • 고객이 검토할 수 있도록 예약 주문서를 반환한다. 이때 API는 반환 결과에 reservation_id를 넣는다. 이 식별자는 전역적 유일성을 보증하는 ID여야 한다.
  3. 검토가 끝난 예약을 전송한다. 이때 요청에도 reservation_id가 포함된다. 이이 값은 예약 테이블의 기본키이기도 하다.

  4. 사용자가 예약 완료 버튼을 한 번 더 누르면 같은 예약이 다시 서버로 전송된다. reservation_id가 예약 테이블의 기본 키이므로, 기본 키의 유일성 조건이 위반되어 새로운 레코드는 생성되지 않는다. 따라서 이중 예약 문제를 해결할 수 있다.

두 번째 시나리오의 경우 사용자A와 사용자B가 하나뿐인 객실을 동시에 예약하려고 할 경우에 대한 문제가 발생한다. 이 문제를 해결하려면 락(Lock)을 사용해야 한다.

먼저 비관적 락 방식은 사용자가 레코드를 갱신하려고 하는 순간 즉시 락을 걸어 동시 업데이트를 방지하는 기술이다. 해당 레코드를 갱신하려는 다른 사용자는 먼저 락을 건 사용자가 변경을 마치고 락을 해제할 때까지 기다려야 한다.

이는 구현이 쉽고 갱신 연산을 직렬화하여 충돌을 막기 때문에 데이터에 대한 경합이 심할 때 유용하다. 하지만 여러 레코드에 락을 걸게 되면 교착 상태가 발생할 수 있다. 또한 트랜잭션이 너무 오랫동안 락을 해제하지 않고 있으면 다른 트랜잭션은 락이 걸린 자원에 접근할 수 없다. 이는 많은 트랜잭션이 해당 자원에 접근하려고 할 때 문제가 된다.

다음으로, 낙관적 락 방식은 여러 사용자가 동시에 같은 자원을 갱신하려 시도하는 것을 허용한다.

이는 일반적으로 version number를 사용하여 구현하는데, 이에 대한 프로세스는 아래 이미지와 같다.

version을 사용한 낙관적락

  1. 데이터베이스 테이블에 version이라는 새 열을 추가한다.

  2. 사용자가 데이터베이스 레코드를 수정하기 전에 애플리케이션은 해당 레코드의 버전 번호를 읽는다.

  3. 사용자가 레코드를 갱신할 때 애플리케이션은 버전 번호에 1을 더한 다음 데이터베이스에 다시 기록한다.

  4. 이때 유효성 검사를 한다. 다음 버전 번호는 현재 버전 번호보다 1만큼 큰 값이어야 한다. 이 유효성 검사가 실패하면 트랜잭션은 중단되고 사용자는 2단계부터 다시 모든 절차를 반복한다.

낙관적 락은 데이터베이스에 락을 걸지 않기 때문에 일반적으로 비관적 락보다 빠르다. 하지만 동시성 수준이 아주 높으면 성능이 급격히 나빠진다.

따라서 낙관적 락은 경쟁이 치열하지 않은 상황에 적합하다. 호텔 예약 시스템의 경우 QPS가 일반적인 상황에선 높지 않기 때문에 낙관적 락 방식을 선택했다. 만약 이벤트나 신규 서비스 출시 등으로 동시성 수준이 높을 경우 다른 대안을 생각해야 한다.

시스템 규모 확장

만약 호텔 예약 시스템이 booking이나 exdepia같은 큰 여행 예약 웹 사이트와 연동되었을 경우를 가정한다.

이런 경우에 QPS는 수 백배 늘어날 수 있다. 이 프로젝트에서 모든 서비스는 무상태(stateless) 서비스이므로 서버를 추가하는 것으로 성능 문제는 해결할 수 있다. 하지만 모든 상태 정보가 저장되는 데이터베이스는 단순히 데이터베이스 서버를 늘릴 경우 데이터의 일관성과 동기화 문제를 해결하기 위한 추가작업이 필요하다.

따라서 데이터베이스의 규모를 늘리는 방법으로 리플리케이션과 캐시를 고려해볼 수 있다. 이 프로젝트에는 읽기 작업(호텔 정보 조회)이 많기 때문에, 샤딩에 비해 구현 난이도가 쉽고 안전성이 높은 리플리케이션 방식을 선택하였다.

리플리케이션 방식의 경우 데이터 정합성에 대한 문제가 발생할 수 있다. 데이터 정합성 문제를 완벽하게 처리하기 위해서는 클러스터링과 같은 동기식 복제 방식을 사용할 수 있지만, 성능을 느리게 만들 수 있다. 따라서 최소한 하나의 복제본이 릴레이 로그의 동기화를 완료했음을 보장하는 반동기 복제 방식을 사용했다. 또한, MHA를 사용하여 Master DB의 장애 발생 시 Master DB의 헬스 체크를 주기적으로 수행하던 Slave DB에서 자동으로 가장 최신 상태의 Slave DB를 Master DB로 승격시키도록 하여 Fail-Over 하였다.

호텔 예약 서비스는 읽기 연산이 쓰기 연산에 비해 훨씬 더 빈번하게 발생한다. 또한, 이러한 서비스에서는 현재와 미래에 대한 정보가 주로 중요하며 과거 데이터의 중요도는 상대적으로 낮다. 이에 따라, 데이터 관리 전략으로 오래된 데이터는 자동으로 제거될 수 있도록 TTL 설정을 적용했으며, 메모리 사용을 최적화하기 위해 LRU 캐시 교체 정책을 도입하였다. 이 프로젝트에서는 캐시를 활용해 읽기 작업을 주로 캐시 서버에서 처리하며, 쓰기 작업은 데이터베이스에 직접 저장하여 처리하도록 설계하였다.

캐시를 도입하기로 했다면 데이터베이스와 캐시 사이의 데이터 일관성 유지에 관한 문제를 고려해야한다.

잔여 객실 데이터에 대한 읽기 연산에는 Look Aside 패턴을 사용하여 캐시 장애가 발생하더라도 서비스에는 문제가 발생하지 않도록 하였다. 하지만 이는 초기 조회 시 캐시 미스가 발생하여 무조건 데이터베이스를 조회해야 하는 단점이 있다. 이러한 단점을 해결하기 위해 cache warming을 하여 캐시에 데이터를 미리 넣어주는 작업을 하도록 하였다.

잔여 객실 데이터에 대한 쓰기 연산에는 데이터베이스에 우선 데이터를 저장하고 typeorm의 subscribers을 사용하여 데이터의 변화(insert, update, delete)가 있을 경우 이를 감지하고 해당 이벤트가 발생 시 캐시 데이터를 업데이트 하도록 설계하였다. 이러한 경우 데이터에 대한 변화를 데이터베이스에 먼저 반영하므로 캐시에는 최신 데이터가 없을 가능성이 있다.

하지만 이런 불일치는 데이터베이스가 최종적으로 잔여 객실 확인을 하도록 하면 문제가 되지 않는다. 캐시 데이터 기준으로는 잔여 객실이 있다고 나오지만 데이터베이스를 기준으로 잔여 객실이 없는 경우를 예로 들어보자. 사용자는 캐시 데이터의 잔여 객실을 기준으로 예약을 시도한다. 그 결과 클라이언트는 다른 사람이 방금 마지막 객실을 예약했다는 오류 메시지를 보게 된다. 사용자가 웹 사이트를 새로 고침하면 데이터베이스와 캐시의 동기화는 끝났을 것이므로 잔여 객실이 없다는 사실을 확인하게 될 것이다.

이런 최종 일관성 모델은 사용자의 경험을 고려해야 한다. 재고의 일관성이 중요한 서비스라면 다른 일관성 유지 방안을 고려해야 한다. 그러나, 이 모델을 통해 시스템의 성능을 크게 향상시킬 수 있으며, 트래픽이 많은 대규모 시스템에서 효과적으로 활용될 수 있다. 만약 중요한 재고의 관리라면 재고를 위한 별도의 실시간 캐시서버를 두는등 조치가 필요하다.

서비스 간 데이터 일관성

전통적인 모놀리스 아키텍처의 경우 데이터의 일관성을 보장하기 위해 관계형 데이터베이스를 공유하는 것이 보통이다. 그러나 이 프로젝트의 설계안으로 채택한 마이크로서비스 기반 아키텍처는 예약 서비스가 예약 및 잔여 객실 API를 모두 담당하도록 하고, 예약 테이블과 잔여 객실 테이블을 동일한 관계형 데이터 베이스에 저장하는 하이브리드 접근법을 선택했다.

먼저 모놀리스 아키텍처의 경우 여러 연산을 하나의 트랜잭션으로 묶어 ACID 속성이 만족되도록 보장할 수 있다. 이에 대한 흐름은 아래와 같다.

모놀리스 아키텍처

하지만 각 서비스가 하나의 데이터베이스를 갖도록 하면, 논리적으로는 하나의 원자적 연산이 여러 데이터베이스에 걸쳐 실행되는 일을 피할 수 없다.

아래 흐름과 같이 예약 데이터베이스 갱신 연산이 실패하였다고 할 때, 잔여 객실 데이터베이스에 기록된 예약 객실 수는 원래 값으로 돌아가야 한다. 모든 플로우가 문제없이 정상적으로 실행되는 경로는 하나뿐이지만, 실패하면 데이터의 불일치 문제가 발생할 수 있는 실행 경로는 많다.

마이크로서비스 아키텍처

물론 이런 일관성 문제를 해결하기 위한 방법(사가 등)은 있지만, 모든 서비스 아키텍처를 마이크로서비스로 하여 데이터 불일치를 해결하기 위해 복잡한 메커니즘을 사용하는것은 시스템 전체 설계의 복잡성을 크게 증가시킨다. 이 프로젝트에선 이런 복잡성 증가가 그만한 가치가 없다고 판단하여 예약 및 잔여 객실 정보를 동일한 관계형 데이터 베이스에 저장하는 실용적이 접근 방식을 선택하였다.

프로젝트 최종 설계안

위의 내용을 토대로 전체적인 프로젝트 아키텍처는 다음과 같다.

호텔 예약 서비스 아키텍처 drawio (1)

hotel_reservation's People

Contributors

ash991213 avatar

Stargazers

winterm1 avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.