SeokRae / 202410_blog_13

0 stars 0 forks source link

가상 면접 사례로 배우는 대규모 시스템 설계 기초 2 - 7장 호텔 예약 시스템 #2

Open SeokRae opened 3 weeks ago

SeokRae commented 3 weeks ago

고민하기

예약 시스템이라는 걸 설계 하기 전, 예약 시스템에 대해 살짝 고민해본다.

예약 시스템이란, 시간이라는 자원에 기반한 서비스로 사용자가 특정 시간대에 특정 리소스를 예약하기 때문에, 시간대별 가용성과 리소스의 상태(사용 가능 여부)를 효율적으로 관리하는 것이 핵심이다.

예약의 상태는 일반적으로 예약 대기 , 예약 확정, 취소됨 등의 다양한 상태로 구분된다. 각 상태에 따른 비즈니스 로직을 구현해야 하며, 상태 변경 시 트랜잭션 일관성을 유지하는 것이 중요하다.

자원이 한정된 경우, 예약 대기를 관리할 수 있다. 특정 시간에 자동으로 예약을 확인하거나 만료 처리하는 스케줄링 시스템이 필요 할 수 있다.

첫 번째 단계: 문제 이해 및 설계 범위 확정

기능적 요구사항, 비기능적 요구사항을 구체화 하기 위한 질의 및 답변을 진행합니다. 포스팅 편의상 결과 내용만 정리하겠습니다.

기능적 요구사항 추출

  1. 시스템 규모 - 5000개 호텔 100만 개 객실을 갖춘 호텔 체인을 위한 웹사이트
  2. 대금결제 방식 - 예약시 대금지불
  3. 예약취소 기능
  4. 초과 예약 기능 - 객실 수 + 10% 초과 예약 가능
  5. API 명세
    • 호텔 정보 페이지 표시
    • 객실 정보 페이지 표시
    • 객실 예약 지원
    • 호텔이나 객실 정보 추가/삭제/갱신하는 관리자 페이지 지원
    • 초과 예약 지원
  6. 객실 가격의 가격변동(Dynamic Pricing)

비기능적 요구사항

  1. 높은 수준의 동시성 지원
  2. 적절한 지연 시간

개략적인 규모 추정

객실 상세 페이지(QPS=300) -> 예약 상세 페이지(QPS=30) -> 객실 예약 페이지(QPS=3) ** 최종 예약 TPS는 3, 예약 페이지의 QPS는 30, 객실 정보 확인 페이지의 QPS는 300

두 번째 단계: 계략적 설계안 제시 및 동의 구하기

API 설계하기

예약 시스템 설계에 필요한 API 명세를 정의하였습니다. 호텔 웹사이트를 구현하기 위해 객실 검색하는 등의 직관적인 기능도 필요하다. 하지만 전체적으로는 필요한 기능일 수 있으나 기술적으로 도전적이지 않은 기능은 현재 상황에서 제외 합니다.

호텔 관련 API

HttpMethod API Endpoint 설명 권한
GET /v1/hotels/:id 호텔의 상세 정보 반환 -
POST /v1/hotels 신규 호텔 추가 호텔 직원만 사용가능
PUT /v1/hotels/:id 호텔 정보 갱신 호텔 직원만 사용가능
DELETE /v1/hotels/:id 호텔 정보 삭제 호텔 직원만 사용가능

객실 관련 API

HttpMethod API Endpoint 설명 권한
GET /v1/hotels/:hotelId/rooms/:roomId 객실 상세 정보 반환 -
POST /v1/hotels/:hotelId/rooms 신규 객실 추가 호텔 직원만 사용가능
PUT /v1/hotels/:hotelId/rooms/:roomId 객실 정보 갱신 호텔 직원만 사용가능
DELETE /v1/hotels/: hotelId/rooms/:roomId 객실 정보 삭제 호텔 직원만 사용가능

예약 관련 API

HttpMethod API Endpoint 설명 권한
GET /v1/reservations 로그인 사용자의 예약 이력 반환 -
POST /v1/reservations/:reservationId 특정 예약의 상세 정보 반환 -
PUT /v1/reservations 신규 예약 -
DELETE /v1/reservations/:reservationId 예약 취소 -
멱등(idempotent key) 키에 대한 이야기

멱등성이란, 특정 작업을 여러 번 반복 실행해도 결과가 동일하게 유지되는 성질을 의미합니다. 즉, 멱등성을 가진 API나 작업은 동일한 요청이 여러 번 반복되어도 상태나 결과에 변함이 없어야 합니다.

멱등성에서 고려해야 하는 상황으로 몇가지가 있습니다.

  1. 상태 변경 여부: 여러 번 요청해도 시스템의 상태가 변하지 않도록 설계.
  2. 중복 요청 처리: 고유한 식별자나 idempotency key를 사용해 중복 처리 방지.
  3. 결과 일관성: 요청이 여러 번 들어와도 일관된 결과를 반환하도록 보장.
  4. 트랜잭션 관리: 동일한 트랜잭션이 중복 처리되지 않도록 관리.
  5. 에러 처리: 실패 시에도 일관된 응답을 반환하고 복구 전략을 포함.

데이터 모델

예약 시스템의 경우, 프로모션과 같은 이벤트로 인한 트래픽이 급증 할 수도 있기 때문에 이에 대한 대비를 해야 한다. 책에서는 관계형 데이터베이스를 선택한다.

  1. 객실을 예약하는 사용자에 비해 호텔 웹 사이트/앱을 방문하는 사용자 수가 압도적으로 많기 때문에 읽기 작업 지원을 잘 제공하는 관계형 데이터베이스를 선택합니다.
  2. 이중 청구 문제, 이중 예약 문제등 ACID 속성을 보장하는 관계형 데이터베이스를 선택합니다.
  3. 엔티티로 비즈니스 데이터의 구조를 명확하게 표현할 수 있는 관계형 데이터베이스를 선택합니다.

초기 설계 시에 어떤 서비스를 생각하고 모델링 하느냐에 따라 다릅니다. 에어비엔비, 호텔 그외 숙박 시설에 따라 객실 유형이 다를 수 있다는 것을 생각해보아야 합니다.

모델링이 되었다면, 서비스에 대한 구성요소를 고려해볼 수 있습니다.

사용자, 관리자(호텔 직원) CDN, 공개 API 게이트 웨이, 내부 API, 호텔 서비스, 요금 서비스, 예약 서비스, 결제 서비스, 호텔 관리 서비스

세 번째 단계: 상세 설계

상세 설계를 진행할 때는 시스템 안정성, 성능, 확장성, 그리고 데이터 일관성을 중점적으로 고려하여 문제를 분석하는 것이 중요합니다. 또한, 장기적인 확장성과 유지보수성을 함께 염두에 두어, 시스템이 성장할 수 있도록 적절한 해결책을 마련하는 것이 필수적입니다.

트러블 슈팅 과정에서는 문제의 빈도와 시스템에 미치는 영향을 기준으로 우선순위를 정해, 발생할 수 있는 문제를 효율적으로 해결해야 합니다. 이를 통해, 설계 단계에서부터 발생 가능한 문제를 미리 대비하고 시스템의 안정성을 높일 수 있습니다.

모델링 개선

호텔 객실 예약 시 특정 객실이 아닌 특정한 객실 유형을 예약하게 됩니다. 호텔의 모든 객실 유형을 담는 테이블에 대한 포인트 였습니다.

동시성 문제를 해결하는 방법

이중 예약이 발생할 수 있는 사례에 대해 두가지 예를 들어 접근해볼 수 있습니다.

  1. 같은 사용자가 예약 버튼을 여러 번 누르는 경우.
  2. 여러 사용자가 동시에 같은 객실을 예약하려는 경우.

동시성 문제를 접근하는 세 가지 방법론

  1. 비관적 락(Pessimistic Lock)
  2. 낙관적 락(Optimistic Lock)
  3. 데이터베이스 제약 조건(constraint)

시스템 규모 확장

예약 시스템이 단일 서비스가 아닌 플랫폼 형태로 확장을 한다면? 어떤 것들을 고려해야 할까?

객실 타입에 대한 확장성은 고려되어 있다는 것을 보장하고, 성능적인 부분만 고민해 보도록 합니다.

  1. 데이터베이스 샤딩
  2. 캐시
  3. 분산 시스템 아키텍처로 인한 서비스 간 데이터 일관성

네 번째 단계: 마무리

호텔 예약 시스템 설계를 진행하며 요구사항을 수집하고, 규모를 추정하여 개략적인 시스템 설계를 완료하였습니다. 이후 API 설계, 데이터 모델링, 시스템 아키텍처 다이어그램을 통해 구체적인 방향을 잡아가던 중, 예약 처리 방식에 대한 관점이 잘못되었음을 확인하였고, 이를 기반으로 데이터 스키마를 수정하는 과정을 거쳤습니다.

서비스의 특성상 경쟁 조건(Race Condition)이 발생할 수 있는 시나리오를 분석하며, 몇 가지 해결책을 제시하였습니다. 특히, 비관적 락(Pessimistic Lock)낙관적 락(Optimistic Lock), 그리고 데이터베이스 제약 조건 등을 사용한 동시성 제어 방안을 검토하였습니다.

또한, 서비스 확장성을 고려하여 성능 향상을 위한 다양한 전략을 살펴보았습니다. 데이터베이스 샤딩과 레디스 캐시를 도입하여 확장성을 극대화할 수 있는 방안을 논의하였고, 시스템 규모 확장에 대한 준비를 철저히 했습니다.

마지막으로, 마이크로서비스 아키텍처(MSA)에서 자주 발생할 수 있는 데이터 일관성 문제에 대해 논의하였으며, 이를 해결하기 위한 몇 가지 전략도 검토하였습니다. 이를 해결하기 위해 2PC(Two-Phase Commit)사가 패턴(Saga Pattern)을 검토하였습니다.

결론적으로, 이번 호텔 예약 시스템 설계를 통해 확장성과 성능, 데이터 일관성을 고려한 시스템 설계를 달성했으며, 이러한 과정을 통해 안정적이고 효율적인 예약 시스템을 구현할 수 있음을 확인하였습니다.

SeokRae commented 3 weeks ago

예외 처리에 대한 일관된 응답을 반환하고, 복구 전략을 포함한다는 말이 무슨말이지?

예외처리와 멱등성 응답을 해야 한다는 상황에 대한 예를 들어보고자합니다.

  1. 예약 시스템에서 POST /v1/reservations 요청이 서버 오류로 인해 실패했습니다.
  2. 클라이언트는 실패 응답을 받고, 일정 시간 후 시스템이 복구된 후 동일한 요청을 다시 보냅니다.
  3. 이때, 멱등성을 보장하는 시스템은 중복된 예약을 방지하기 위해 reservationID와 같은 식별자를 사용하여 이전 요청이 처리되지 않았음을 인지하고, 동일한 요청을 성공적으로 처리합니다.
SeokRae commented 3 weeks ago

동시성 문제

동시성 문제는 예약 서비스에서 발생할 수 있는 중요한 이슈 중 하나입니다. 특히 이중 예약 문제는 다음과 같은 상황에서 발생할 수 있습니다:

1. 같은 사용자가 예약 버튼을 여러 번 누르는 경우.
2. 여러 사용자가 동시에 같은 객실을 예약하려는 경우.

이 문제를 해결하기 위해 여러 가지 접근 방법이 있습니다.

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

클라이언트 측 구현 방안

가장 간단한 방법은 클라이언트 측에서 중복 요청을 방지하는 것입니다. 예약 요청을 보낼 때, 버튼을 비활성화하거나, disabled 상태로 만들어 중복 클릭을 방지하는 방식입니다. 하지만 이 방식은 클라이언트 측에서만 처리되기 때문에, 자바스크립트를 우회할 수 있는 경우에 한계가 있습니다.

멱등성(idempotent) API 적용

보다 안정적인 방법은 서버 측에서 멱등성을 보장하는 것입니다. 이를 위해 예약 API에 멱등 키(idempotency key)를 추가할 수 있습니다. 예를 들어, 예약 생성 시 reservationId를 멱등 키로 활용할 수 있습니다.

  1. 예약 요청 시 서버는 예약을 처리하고, reservationId를 응답합니다.
  2. 예약 API 요청이 중복되었을 때, reservationId는 유일한 값이므로 중복된 요청은 데이터베이스에서 PK 제약 조건에 의해 거부됩니다.
  3. 첫 번째 요청이 정상 처리되었을 경우, 재요청이 들어와도 새롭게 레코드가 생성되지 않으므로 이중 예약을 방지할 수 있습니다.

요약

2. 여러 사용자가 잔여 객실이 하나밖에 없는 유형의 객실을 동시에 예약하는 경우

해당 경우는 제한된 자원을 여러 클라이언트가 점유하려고 하는 상황에서 발생할 수 있는 문제입니다.

상황을 예를 들어보면 아래와 같습니다.

  1. 트랜잭션 1이 예약을 처리하는 동안, 트랜잭션 1의 변경 사항(commit)이 완료되기 전에 트랜잭션 2가 실행됩니다.
  2. 이때, 트랜잭션 2는 트랜잭션 1의 결과를 보지 못한 상태에서 예약 작업을 진행하게 되어 동일한 자원을 예약하려고 시도하게 됩니다.
  3. 트랜잭션 1에서 총 예약된 방의 수(total_reserved)는 99로 계산되었지만, 트랜잭션 2는 이 값을 보지 못하고 동일한 계산을 진행하여 방을 예약하게 됩니다.
  4. 결국 두 트랜잭션이 모두 성공적으로 완료되면 예약된 방의 수는 100을 초과하게 되어 이중 예약 문제가 발생합니다.

** 이 문제를 해결하려면 잠금 메커니즘(lock)을 사용하여 트랜잭션 간의 충돌을 방지해야 합니다.

이를 해결 하는 방법으로 두 가지 방식을 제안합니다.

  1. 잔여 객실을 확인하는 쿼리를 실행하여 예약 가능 여부를 확인하는 방법
  2. 실제 예약을 실행하는 쿼리를 실행하여 total_reserved 값을 업데이트 하는 방법

잔여 객실을 확인하는 쿼리를 실행하여 예약 가능 여부를 확인하는 방법

⚠️ 이 시점에 비관적 락을 사용하여 조회된 데이터가 다른 트랜잭션에 의해 수정되지 않도록 잠금을 걸어다른 트랜잭션이 접근하는 것을 방지하는 방법 ⚠️ FOR UPDATE 절을 사용하여, 조회된 레코드에 잠금을 걸어 다른 트랜잭션이 동시에 이 데이터를 수정하지 못하도록 합니다. ⚠️ 잔여 객실 확인 단계에서는 비관적 락 사용 시 잠금 기간 관리, 데드락 방지, 동시성 문제로 인한 성능 저하에 주의해야 합니다.

실제 예약을 실행하여 total_reserved 값을 업데이트하는 방법

⚠️ 실제 예약 실행 단계에서는 트랜잭션 일관성 유지, 낙관적 락 충돌 관리, 대량 트랜잭션 처리 성능 최적화를 신경 써야 합니다.

SeokRae commented 3 weeks ago

멱등성이란?

동일한 연산을 여러 번 수행하더라도 결과가 변하지 않는 성질입니다. 즉, 같은 요청을 여러 번 보내더라도 시스템의 상태나 응답이 달라지지 않는 것을 보장하는 개념입니다.

HttpMethod를 기준으로 설명을 해본다면,

GET의 경우에는 여러번 호출해도 결과가 동일하므로 멱등성을 갖는다고 할 수 있습니다. PUT의 경우 동일한 데이터를 여러 번 수정하는 경우라도 최종 상태는 동일하기 때문에 결과가 변하지 않습니다.

실 사례의 예시를 들어보면 결제에 대한 중복 요청이 들어오는 경우 동일한 결제가 여러 번 발생하지 않고, 한 번만 처리되도록 멱등성 키를 사용하여 중복 요청을 방지해야 합니다.

중요한 핵심적인 속성

멱등성이 왜 고려되어야 하는가?

  1. 네트워크 및 시스템 장애 대비
  1. 데이터 무결성 유지
  1. 사용자 경험 개선
  1. 분산 시스템에서의 신뢰성

그래서 멱등성을 어떻게 활용하는데?

기본적으로 HTTP POST 메서드는 멱등성이 보장되지 않습니다. POST 요청은 서버에 데이터를 생성하거나, 수정하는 데 사용되기 때문에 동일한 요청이 반복될 경우 중복 데이터가 생성되거나 예상치 않은 결과가 발생할 수 있습니다.

하지만, 멱등성 키(idempotent key)를 사용하는 방식은 이 문제를 해결하는 방법입니다. 멱등성 키는 동일한 요청이 여러 번 발생하더라도, 서버가 이를 동일한 요청으로 인식하고, 요청이 처음 성공한 결과를 반복해서 반환함으로써 멱등성을 보장하는 역할을 합니다. 이렇게 하면 POST 요청에서 발생할 수 있는 중복 작업이나 부작용을 방지할 수 있습니다.

다시 한번 정리하면, 기본적으로 HTTP POST 메서드는 멱등성을 보장하지 않지만, 멱등성 키(idempotency key)를 사용함으로써 동일한 요청을 여러 번 반복해도 서버가 이를 인식하고 동일한 결과를 반환하도록 멱등성을 보장할 수 있습니다. 이를 통해 중복 데이터 생성이나 불필요한 업데이트를 방지할 수 있습니다.

references

SeokRae commented 2 weeks ago

설계 시에 DB를 선택하는 기준

현재 예약 서비스를 설명하는 책의 기준으로는 관계형 데이터베이스의 ACID 속성의 장점과 명확한 표현이 가능한 데이터 모델링 그리고 읽기 빈도가 쓰기 연산에 높은 작업 흐름을 잘 지원한다는 이유에서 관계형 데이터베이스를 권장합니다.

1. 데이터 모델링:

2. 확장성(Scalability):

3. 읽기 성능(Read Performance):

4. 트랜잭션 및 일관성(Transaction & Consistency):

5. 복잡한 쿼리 지원:

6. 데이터 크기 및 구조:

7. 사용 사례(Use Case):

SeokRae commented 2 weeks ago

비관적 락(Pessimistic Lock)

데이터베이스 트랜잭션에서 동시성 문제를 방지하기 위해 사용하는 잠금 메커니즘으로 트랜잭션이 데이터를 읽거나 수정하기 전에 해당 데이터를 잠금(Lock) 상태로 설정하여 다른 트랜잭션이 해당 데이터에 접근하지 못하도록 합니다.

Pessimistic Lock의 동작 방식

  1. 잠금 설정: 트랜잭션이 데이터를 수정하거나 읽으려는 순간, 그 데이터에 대해 잠금(Lock)이 설정됩니다.
  2. 다른 트랜잭션 차단: 다른 트랜잭션이 같은 데이터를 읽거나 수정하려고 할 때, 잠금이 해제되기 전까지 대기 상태에 들어갑니다. 이는 충돌이나 데이터 불일치를 방지하기 위함입니다.
  3. 잠금 해제: 트랜잭션이 완료되거나 롤백될 때 잠금이 해제되며, 그 이후에 다른 트랜잭션들이 해당 데이터에 접근할 수 있습니다.

Pessimistic Lock의 동작 원리

  1. 트랜잭션 시작: 트랜잭션이 실행되면 특정 데이터를 읽거나 수정하려고 할 때, 그 데이터에 대해 잠금이 걸립니다. 이 잠금은 데이터가 변경될 수 있는 가능성을 미리 차단하는 것입니다.

  2. 잠금 유형: 두 가지 주요 유형의 잠금이 존재합니다.

    • 읽기 잠금(Shared Lock): 데이터 읽기를 위해 설정됩니다. 여러 트랜잭션이 동시에 읽을 수 있지만, 데이터 변경은 불가능합니다.
    • 쓰기 잠금(Exclusive Lock): 데이터 수정이 필요한 경우 설정되며, 이 잠금이 설정된 동안 다른 트랜잭션은 그 데이터를 읽거나 수정할 수 없습니다.
  3. 다른 트랜잭션 대기: 데이터에 잠금이 설정된 동안 다른 트랜잭션이 동일한 데이터에 접근하려고 하면, 해당 트랜잭션은 잠금이 해제될 때까지 대기 상태로 전환됩니다.

  4. 트랜잭션 완료 및 잠금 해제: 트랜잭션이 완료되면(Commit 또는 Rollback) 데이터에 설정된 잠금이 해제되며, 대기 중이던 다른 트랜잭션이 그 데이터에 접근할 수 있게 됩니다.

  5. 데드락(Deadlock) 관리: Pessimistic Lock에서는 여러 트랜잭션이 서로 다른 자원에 대한 잠금을 대기하는 상황에서 데드락이 발생할 수 있습니다. 이를 해결하기 위해, 일반적으로 타임아웃을 설정하거나, 교착 상태 감지 알고리즘을 사용하여 데드락을 방지합니다.

Pessimistic Lock의 특징

Pessimistic Lock 사용 시기