대규모 시스템 설계 기초 2 (7장)
가상 면접 사례로 배우는 대규모 시스템 설계 기초 2 (7장) 의 내용 중, 인상적이었던 부분을 발췌 및 요약합니다.
호텔 예약 시스템
데이터 모델
어떤 데이터베이스를 사용해야 할지 결정해보자. 호텔 예약 시스템은 다음 질의를 지원해야 할 것이다.
- 호텔 상세 정보 확인
- 지정된 날짜 범위에 사용 가능한 객실 유형 확인
- 예약 정보 기록
- 예약 내역 또는 과거 예약 이력 정보 조회
결과적으로, 위 요건을 고려하면 관계형 데이터베이스를 사용할 것인데 다음과 같은 이유 때문이다.
- 해당 서비스는 읽기 빈도가 쓰기 빈도에 비해 높기 때문이다.
- 관계형 DB 는 ACID 속성을 보장한다. ACID 속성은 예약 시스템에 매우 중요하다.
- 데이터를 쉽게 모델링할 수 있으므로 비즈니스 데이터의 구조를 명확히 표현할 수 있다.
개선된 데이터 모델
호텔 객식을 예약할 때는 특정 객실이 아니라, 특정 호텔의 특정 객실 유형을 예약한다. 이를 해결하기 위해서는 특정 객실 유형의 특정 일자 요금 정보를 담을 수 있도록 모델링을 해야 할 것이다.
2년 이내 모든 미래 날짜에 대한 가용 데이터를 채워놓는다고 가정해보자. 그럼에도 레코드의 수는 약 5000 개 호텔 * 20 개 객실 유형 * 2년 * 365 = 7300만개 정도이다.
이 정도는 하나의 데이터베이스에 저장해도 될 것이다. 물론 db 서버가 하나면 SPOF 문제가 발생할 것이다. 만약 데이터를 줄여야 한다면 어떻게 해야 할까?
- 현재 및 향후 예약 데이터만 저장하고, 예약 이력은 냉동 저장소로 옮겨도 된다.
- 데이터베이스를 샤딩한다. 호텔 키가 샤딩 키가 되면 될 것이다.
동시성 문제
가장 중요한 문제는 이중 예약을 방지하는 것이다. 두 가지 상황이 예상된다.
- 같은 사용자가 예약 버튼을 여러 번 누를 수 있다.
- 여러 사용자가 같은 객실을 동시에 예약하려 할 수 있다.
첫 번째 문제는 클라이언트 구현으로 막거나, 먹등 API 를 설계하면 된다. 예약 주문서를 고객이 생성하는 시점에, 멱등 key 를 발급받고, 해당 key 로 예약 프로세스를 진행하게 하면 된다.
- 예약 주문서를 만든다.
- 예약 주문서 API 반환 결과에는 멱등 key 가 될 수 있는 reservation_id 가 존재한다.
- 검토가 끝나면, 예약을 전송하고 이 떄 reservation_id 가 붙는다.
- 사용자가 예약 완료 버튼을 다시 누르더라도, pk 인 reservation_id 가 중복되어 후행 요청은 무시된다.
두 번째 문제는 어떻게 해결할까? 데이터 베이스 트랜잭션 격리 수준이 serializable 로 설정되어 있지 않다면 여러 사용자의 트랜잭션이 모두 db 에 반영되어, 중복 예약이 발생할 것이다.
이 문제를 해결하기 위해서는 락을 사용할 수 밖에 없다.
비관적 락 방식
동시성 제어 방안으로, 사용자가 레코드를 갱신하려고 하는 순간 락을 거는 것이다. Mysql 의 경우 select … for update 문을 실행하면, select 가 반환한 레코드에 락이 걸린다. 다른 트랜잭션에서 해당 row 에 접근해도, 첫 번째 트랜잭션이 끝날 때까지 대기할 수 밖에 없다.
그러나 이 방식은 데드락 문제를 발생시킬 수 있고, 확장성도 낮다.
낙관적 락
버전번호나 타임스탬프 방식을 사용할 수 있지만, 버전 번호 방식을 더 나은 선택지로 본다.
- db 테이블에 version column 을 추가한다.
- 사용자가 레코드를 수정하기 전에, 해당 레코드의 버전 번호를 확인한다.
- 사용자가 레코드를 갱신할 때, 버전 번호에 1 을 더한 후 저장한다.
- 이 때, 유효성 검사를 하는데 다음 버전 번호가 이전 번호 보다 1만큼 큰 값이어야 한다. 유효성 검사가 실패하면 트랜잭션은 중단(abort)된다.
낙관적 락은 비관적 락보다 빠르다. 유효하지 않은 데이터가 db 에 저장될 경우가 없다. 경쟁이 치열하지 않은 상황에서 락을 관리하는 비용 없이 트랜잭션을 실행할 수 있을 것이다.
캐시
시스템 성능을 향상시키기 위해 캐시를 도입할 수 있을 것이다. 레디스 캐시를 사용한다면 과거의 객실 데이터가 자동으로 소멸되게끔 관리할 수 있을 것이다. 문제는 잔여 객실이 캐시 상에는 충분하더라도, db 에서 확인을 해야만 한다는 것이다.
- 잔여 객실 수를 캐시에서 db 로 질의하여 확인한다.
- 잔여 객실 데이터를 갱신한다. 캐시에는 비동기적으로 변경 내역이 반영되어야 한다.
캐시에는 최신 데이터가 없을 가능성이 있다. 그러나, 고객이 예약을 진행하는 프로세스에서는 db 질의를 통해서 잔여 객실 여부를 확인할 것이므로 이 불일치 문제를 너무 걱정하지 않아도 될 것이다.
MSA 구조와 db
만약 각각의 서비스마다 별개의 db 를 가져야 한다고 가정해보자. 그렇다면 하나의 논리적인 원자적 연산이 여러 db 에 걸쳐 실행된다. 이를 위해서는 분산 트랜잭션 관리를 해야 한다.
2단계 커밋 방법을 사용할 수도 있지만, 성능이 좋지 못하다. Saga 와 같은 방법을 사용해보자.