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 데이터 모델
호텔 예약 시스템 흐름
- 호텔 상세 정보 확인
- 지정된 날짜 범위에 사용 가능한 객실 유형 확인
- 예약 정보 기록
- 예약 내역 또는 과거 예약 이력 정보 조회
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, 그리고 날짜에 맞는 잔여 객실 수
잔여 객실 데이터베이스
- 잔여 객실 수에 대한 가장 신뢰성 있는 데이터가 보관된 장소
캐시가 주는 과제
캐시 계층이 추가되면서 데이터베이스와 캐시 사이의 데이터 일관성 유지를 고려해야 한다. 사용자가 객실을 예약할 때 아무 문제가 없는 경우 다음 두 가지 작업이 이루어진다
- 잔여 객실 수를 캐시로부터 획득
- 예약을 한 경우 DB에 바로 반영하고 캐시에 남은 객실 수를 비동기로 적용
CDC 메커니즘
- 데이터베이스에서 발생한 변화를 감지하여 해당 변경 내역을 다른 시스템에 적용하는 메커니즘
- Debezium이 하나의 예시, RedHat에서 지원하는 Apache Kafka 기반의 분산 CDC 플랫폼이고 Kafka connect의 소스 커넥터
- DB의 변경사항을 캡쳐하여 Kafka의 Topic에 변경사항을 발행함
비동기로 하는 경우 문제?
- 사용자가 남은 객실을 보고 바로 예약을 시도하는 경우가 있을 것
- 비동기로 인해 캐시 데이터 갱신이 되지 않아 예약을 시도하지만 유효성 검사에서 걸려 예약에 실패함
- 사용자는 객실 결제에 실패했다기 보단 그 사이에 누군가 예약했구나라고 생각할 수 있기에 UX가 크게 떨어지지 않음
서비스 간 데이터 일관성
모놀리식 서비스
- 모놀리식 아키텍처는 데이터 일관성을 위해 관계형 데이터베이스를 공유함
- 잔여 객실 현황 관리, 객실 예약을 하나의 트랜잭션에서 모두 수행하기 때문에 동시성 문제를 효과적으로 해결

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

'ETC > 가상 면접 사례로 배우는 대규모 시스템 설계 기초 2' 카테고리의 다른 글
| 5장 지표 모니터링 및 경보 시스템 (0) | 2025.07.08 |
|---|