포스트

대규모 시스템 설계 기초 2 (7장)

가상 면접 사례로 배우는 대규모 시스템 설계 기초 2 (7장) 의 내용 중, 인상적이었던 부분을 발췌 및 요약합니다.

호텔 예약 시스템

데이터 모델

어떤 데이터베이스를 사용해야 할지 결정해보자. 호텔 예약 시스템은 다음 질의를 지원해야 할 것이다.

  • 호텔 상세 정보 확인
  • 지정된 날짜 범위에 사용 가능한 객실 유형 확인
  • 예약 정보 기록
  • 예약 내역 또는 과거 예약 이력 정보 조회

결과적으로, 위 요건을 고려하면 관계형 데이터베이스를 사용할 것인데 다음과 같은 이유 때문이다.

  • 해당 서비스는 읽기 빈도가 쓰기 빈도에 비해 높기 때문이다.
  • 관계형 DB 는 ACID 속성을 보장한다. ACID 속성은 예약 시스템에 매우 중요하다.
  • 데이터를 쉽게 모델링할 수 있으므로 비즈니스 데이터의 구조를 명확히 표현할 수 있다.

개선된 데이터 모델

호텔 객식을 예약할 때는 특정 객실이 아니라, 특정 호텔의 특정 객실 유형을 예약한다. 이를 해결하기 위해서는 특정 객실 유형의 특정 일자 요금 정보를 담을 수 있도록 모델링을 해야 할 것이다.
2년 이내 모든 미래 날짜에 대한 가용 데이터를 채워놓는다고 가정해보자. 그럼에도 레코드의 수는 약 5000 개 호텔 * 20 개 객실 유형 * 2년 * 365 = 7300만개 정도이다.
이 정도는 하나의 데이터베이스에 저장해도 될 것이다. 물론 db 서버가 하나면 SPOF 문제가 발생할 것이다. 만약 데이터를 줄여야 한다면 어떻게 해야 할까?

  • 현재 및 향후 예약 데이터만 저장하고, 예약 이력은 냉동 저장소로 옮겨도 된다.
  • 데이터베이스를 샤딩한다. 호텔 키가 샤딩 키가 되면 될 것이다.

동시성 문제

가장 중요한 문제는 이중 예약을 방지하는 것이다. 두 가지 상황이 예상된다.

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

첫 번째 문제는 클라이언트 구현으로 막거나, 먹등 API 를 설계하면 된다. 예약 주문서를 고객이 생성하는 시점에, 멱등 key 를 발급받고, 해당 key 로 예약 프로세스를 진행하게 하면 된다.

  1. 예약 주문서를 만든다.
  2. 예약 주문서 API 반환 결과에는 멱등 key 가 될 수 있는 reservation_id 가 존재한다.
  3. 검토가 끝나면, 예약을 전송하고 이 떄 reservation_id 가 붙는다.
  4. 사용자가 예약 완료 버튼을 다시 누르더라도, pk 인 reservation_id 가 중복되어 후행 요청은 무시된다.


두 번째 문제는 어떻게 해결할까? 데이터 베이스 트랜잭션 격리 수준이 serializable 로 설정되어 있지 않다면 여러 사용자의 트랜잭션이 모두 db 에 반영되어, 중복 예약이 발생할 것이다.
이 문제를 해결하기 위해서는 락을 사용할 수 밖에 없다.

비관적 락 방식

동시성 제어 방안으로, 사용자가 레코드를 갱신하려고 하는 순간 락을 거는 것이다. Mysql 의 경우 select … for update 문을 실행하면, select 가 반환한 레코드에 락이 걸린다. 다른 트랜잭션에서 해당 row 에 접근해도, 첫 번째 트랜잭션이 끝날 때까지 대기할 수 밖에 없다.
그러나 이 방식은 데드락 문제를 발생시킬 수 있고, 확장성도 낮다.

낙관적 락

버전번호나 타임스탬프 방식을 사용할 수 있지만, 버전 번호 방식을 더 나은 선택지로 본다.

  1. db 테이블에 version column 을 추가한다.
  2. 사용자가 레코드를 수정하기 전에, 해당 레코드의 버전 번호를 확인한다.
  3. 사용자가 레코드를 갱신할 때, 버전 번호에 1 을 더한 후 저장한다.
  4. 이 때, 유효성 검사를 하는데 다음 버전 번호가 이전 번호 보다 1만큼 큰 값이어야 한다. 유효성 검사가 실패하면 트랜잭션은 중단(abort)된다.

낙관적 락은 비관적 락보다 빠르다. 유효하지 않은 데이터가 db 에 저장될 경우가 없다. 경쟁이 치열하지 않은 상황에서 락을 관리하는 비용 없이 트랜잭션을 실행할 수 있을 것이다.

캐시

시스템 성능을 향상시키기 위해 캐시를 도입할 수 있을 것이다. 레디스 캐시를 사용한다면 과거의 객실 데이터가 자동으로 소멸되게끔 관리할 수 있을 것이다. 문제는 잔여 객실이 캐시 상에는 충분하더라도, db 에서 확인을 해야만 한다는 것이다.

  1. 잔여 객실 수를 캐시에서 db 로 질의하여 확인한다.
  2. 잔여 객실 데이터를 갱신한다. 캐시에는 비동기적으로 변경 내역이 반영되어야 한다.

캐시에는 최신 데이터가 없을 가능성이 있다. 그러나, 고객이 예약을 진행하는 프로세스에서는 db 질의를 통해서 잔여 객실 여부를 확인할 것이므로 이 불일치 문제를 너무 걱정하지 않아도 될 것이다.

MSA 구조와 db

만약 각각의 서비스마다 별개의 db 를 가져야 한다고 가정해보자. 그렇다면 하나의 논리적인 원자적 연산이 여러 db 에 걸쳐 실행된다. 이를 위해서는 분산 트랜잭션 관리를 해야 한다.
2단계 커밋 방법을 사용할 수도 있지만, 성능이 좋지 못하다. Saga 와 같은 방법을 사용해보자.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.