Open SeokRae opened 3 weeks ago
예외처리와 멱등성 응답을 해야 한다는 상황에 대한 예를 들어보고자합니다.
재시도 매커니즘 시스템이 장애에서 복구될 때, 클라이언트 또는 서버가 재시도를 할 수 있는 로직을 구현 클라이언트는 요청이 실패했을 때 지수적 백오프(exponential backoff)를 사용해 일정 시간 간격으로 재시도
보류된 작업 처리 서버는 작업이 실패했을 때 요청을 큐에 넣어두고, 장애가 해결되면 다시 처리할 수 있는 전략을 사용
에러 응답의 명확성 실패 시에도 요청이 어떤 상태에 있는지 명확히 설명하는 응답을 반환 작업이 처리 중인지, 시스템 장애로 실패했는지, 혹은 요청이 거절되었는지를 클라이언트가 명확하게 인지할 수 있어야 합니다.
동시성 문제는 예약 서비스에서 발생할 수 있는 중요한 이슈 중 하나입니다. 특히 이중 예약 문제는 다음과 같은 상황에서 발생할 수 있습니다:
1. 같은 사용자가 예약 버튼을 여러 번 누르는 경우.
2. 여러 사용자가 동시에 같은 객실을 예약하려는 경우.
이 문제를 해결하기 위해 여러 가지 접근 방법이 있습니다.
가장 간단한 방법은 클라이언트 측에서 중복 요청을 방지하는 것입니다. 예약 요청을 보낼 때, 버튼을 비활성화하거나, disabled 상태로 만들어 중복 클릭을 방지하는 방식입니다. 하지만 이 방식은 클라이언트 측에서만 처리되기 때문에, 자바스크립트를 우회할 수 있는 경우에 한계가 있습니다.
보다 안정적인 방법은 서버 측에서 멱등성을 보장하는 것입니다. 이를 위해 예약 API에 멱등 키(idempotency key)를 추가할 수 있습니다. 예를 들어, 예약 생성 시 reservationId를 멱등 키로 활용할 수 있습니다.
해당 경우는 제한된 자원을 여러 클라이언트가 점유하려고 하는 상황에서 발생할 수 있는 문제입니다.
상황을 예를 들어보면 아래와 같습니다.
** 이 문제를 해결하려면 잠금 메커니즘(lock)을 사용하여 트랜잭션 간의 충돌을 방지해야 합니다.
이를 해결 하는 방법으로 두 가지 방식을 제안합니다.
잔여 객실을 확인하는 쿼리를 실행하여 예약 가능 여부를 확인하는 방법
⚠️ 이 시점에 비관적 락을 사용하여 조회된 데이터가 다른 트랜잭션에 의해 수정되지 않도록 잠금을 걸어다른 트랜잭션이 접근하는 것을 방지하는 방법 ⚠️ FOR UPDATE 절을 사용하여, 조회된 레코드에 잠금을 걸어 다른 트랜잭션이 동시에 이 데이터를 수정하지 못하도록 합니다. ⚠️ 잔여 객실 확인 단계에서는 비관적 락 사용 시 잠금 기간 관리, 데드락 방지, 동시성 문제로 인한 성능 저하에 주의해야 합니다.
실제 예약을 실행하여 total_reserved 값을 업데이트하는 방법
⚠️ 실제 예약 실행 단계에서는 트랜잭션 일관성 유지, 낙관적 락 충돌 관리, 대량 트랜잭션 처리 성능 최적화를 신경 써야 합니다.
동일한 연산을 여러 번 수행하더라도 결과가 변하지 않는 성질입니다. 즉, 같은 요청을 여러 번 보내더라도 시스템의 상태나 응답이 달라지지 않는 것을 보장하는 개념입니다.
HttpMethod를 기준으로 설명을 해본다면,
GET의 경우에는 여러번 호출해도 결과가 동일하므로 멱등성을 갖는다고 할 수 있습니다. PUT의 경우 동일한 데이터를 여러 번 수정하는 경우라도 최종 상태는 동일하기 때문에 결과가 변하지 않습니다.
실 사례의 예시를 들어보면 결제에 대한 중복 요청이 들어오는 경우 동일한 결제가 여러 번 발생하지 않고, 한 번만 처리되도록 멱등성 키를 사용하여 중복 요청을 방지해야 합니다.
기본적으로 HTTP POST 메서드는 멱등성이 보장되지 않습니다. POST 요청은 서버에 데이터를 생성하거나, 수정하는 데 사용되기 때문에 동일한 요청이 반복될 경우 중복 데이터가 생성되거나 예상치 않은 결과가 발생할 수 있습니다.
하지만, 멱등성 키(idempotent key)를 사용하는 방식은 이 문제를 해결하는 방법입니다. 멱등성 키는 동일한 요청이 여러 번 발생하더라도, 서버가 이를 동일한 요청으로 인식하고, 요청이 처음 성공한 결과를 반복해서 반환함으로써 멱등성을 보장하는 역할을 합니다. 이렇게 하면 POST 요청에서 발생할 수 있는 중복 작업이나 부작용을 방지할 수 있습니다.
다시 한번 정리하면, 기본적으로 HTTP POST 메서드는 멱등성을 보장하지 않지만, 멱등성 키(idempotency key)를 사용함으로써 동일한 요청을 여러 번 반복해도 서버가 이를 인식하고 동일한 결과를 반환하도록 멱등성을 보장할 수 있습니다. 이를 통해 중복 데이터 생성이나 불필요한 업데이트를 방지할 수 있습니다.
현재 예약 서비스를 설명하는 책의 기준으로는 관계형 데이터베이스의 ACID 속성의 장점과 명확한 표현이 가능한 데이터 모델링 그리고 읽기 빈도가 쓰기 연산에 높은 작업 흐름을 잘 지원한다는 이유에서 관계형 데이터베이스를 권장합니다.
RDBMS: 관계형 데이터베이스는 정규화된 테이블로 데이터를 구조화하며, 정해진 스키마가 있습니다. 각 데이터 항목은 테이블 간의 관계를 통해 서로 연결될 수 있습니다. 이는 데이터 무결성을 보장하고, 복잡한 관계형 쿼리를 효율적으로 처리합니다.
NoSQL: NoSQL 데이터베이스는 비정형 또는 반정형 데이터를 다룰 수 있는 유연한 데이터 모델을 제공합니다. 문서 기반, 키-값, 열 기반, 그래프 데이터 모델 등 다양한 형태로 데이터를 저장할 수 있으며, 스키마리스(Schema-less) 방식이 일반적입니다.
RDBMS: 전통적으로 수직적 확장(Vertical Scaling)을 지원합니다. 즉, 더 많은 데이터를 처리하기 위해 더 강력한 서버로 업그레이드해야 합니다.
NoSQL: 수평적 확장(Horizontal Scaling)을 지원하는 구조입니다. 여러 서버에 데이터를 분산하여 확장할 수 있으므로, 대규모 트래픽을 효율적으로 처리할 수 있습니다.
RDBMS: 잘 최적화된 경우, 읽기 성능이 뛰어날 수 있습니다. 그러나 트랜잭션 무결성을 유지하기 위한 ACID 특성 때문에 트랜잭션이 많을 경우 성능에 부하가 생길 수 있습니다.
NoSQL: 기본적으로 읽기 성능에 초점을 맞춘 데이터베이스가 많습니다. 특히 캐시 기능과 복제 기능을 통해 조회 성능을 향상시킬 수 있습니다.
RDBMS: RDBMS는 ACID(Atomicity, Consistency, Isolation, Durability)를 준수하여 트랜잭션의 일관성을 보장합니다. 이는 트랜잭션이 매우 중요한 시스템(예: 금융 시스템)에서 적합합니다.
NoSQL: 대부분의 NoSQL 데이터베이스는 BASE(Basically Available, Soft state, Eventually consistent) 특성을 가지고 있어 일관성보다는 가용성에 더 초점을 맞춥니다.
데이터베이스 트랜잭션에서 동시성 문제를 방지하기 위해 사용하는 잠금 메커니즘으로 트랜잭션이 데이터를 읽거나 수정하기 전에 해당 데이터를 잠금(Lock) 상태로 설정하여 다른 트랜잭션이 해당 데이터에 접근하지 못하도록 합니다.
트랜잭션 시작: 트랜잭션이 실행되면 특정 데이터를 읽거나 수정하려고 할 때, 그 데이터에 대해 잠금이 걸립니다. 이 잠금은 데이터가 변경될 수 있는 가능성을 미리 차단하는 것입니다.
잠금 유형: 두 가지 주요 유형의 잠금이 존재합니다.
다른 트랜잭션 대기: 데이터에 잠금이 설정된 동안 다른 트랜잭션이 동일한 데이터에 접근하려고 하면, 해당 트랜잭션은 잠금이 해제될 때까지 대기 상태로 전환됩니다.
트랜잭션 완료 및 잠금 해제: 트랜잭션이 완료되면(Commit 또는 Rollback) 데이터에 설정된 잠금이 해제되며, 대기 중이던 다른 트랜잭션이 그 데이터에 접근할 수 있게 됩니다.
데드락(Deadlock) 관리: Pessimistic Lock에서는 여러 트랜잭션이 서로 다른 자원에 대한 잠금을 대기하는 상황에서 데드락이 발생할 수 있습니다. 이를 해결하기 위해, 일반적으로 타임아웃을 설정하거나, 교착 상태 감지 알고리즘을 사용하여 데드락을 방지합니다.
고민하기
예약 시스템이라는 걸 설계 하기 전, 예약 시스템에 대해 살짝 고민해본다.
예약 시스템
이란, 시간이라는 자원에 기반한 서비스로 사용자가 특정 시간대에 특정 리소스를 예약하기 때문에, 시간대별 가용성과 리소스의 상태(사용 가능 여부)를 효율적으로 관리하는 것이 핵심이다.예약의 상태는 일반적으로 예약 대기 , 예약 확정, 취소됨 등의 다양한 상태로 구분된다. 각 상태에 따른 비즈니스 로직을 구현해야 하며, 상태 변경 시 트랜잭션 일관성을 유지하는 것이 중요하다.
자원이 한정된 경우, 예약 대기를 관리할 수 있다. 특정 시간에 자동으로 예약을 확인하거나 만료 처리하는 스케줄링 시스템이 필요 할 수 있다.
첫 번째 단계: 문제 이해 및 설계 범위 확정
기능적 요구사항, 비기능적 요구사항을 구체화 하기 위한 질의 및 답변을 진행합니다. 포스팅 편의상 결과 내용만 정리하겠습니다.
기능적 요구사항 추출
비기능적 요구사항
개략적인 규모 추정
객실 상세 페이지(QPS=300) -> 예약 상세 페이지(QPS=30) -> 객실 예약 페이지(QPS=3) ** 최종 예약 TPS는 3, 예약 페이지의 QPS는 30, 객실 정보 확인 페이지의 QPS는 300
두 번째 단계: 계략적 설계안 제시 및 동의 구하기
API 설계하기
예약 시스템 설계에 필요한 API 명세를 정의하였습니다. 호텔 웹사이트를 구현하기 위해 객실 검색하는 등의 직관적인 기능도 필요하다. 하지만 전체적으로는 필요한 기능일 수 있으나 기술적으로 도전적이지 않은 기능은 현재 상황에서 제외 합니다.
호텔 관련 API
객실 관련 API
예약 관련 API
멱등(idempotent key) 키에 대한 이야기
멱등성이란, 특정 작업을 여러 번 반복 실행해도 결과가 동일하게 유지되는 성질을 의미합니다. 즉, 멱등성을 가진 API나 작업은 동일한 요청이 여러 번 반복되어도 상태나 결과에 변함이 없어야 합니다.
멱등성에서 고려해야 하는 상황으로 몇가지가 있습니다.
데이터 모델
예약 시스템의 경우, 프로모션과 같은 이벤트로 인한 트래픽이 급증 할 수도 있기 때문에 이에 대한 대비를 해야 한다. 책에서는 관계형 데이터베이스를 선택한다.
초기 설계 시에 어떤 서비스를 생각하고 모델링 하느냐에 따라 다릅니다. 에어비엔비, 호텔 그외 숙박 시설에 따라 객실 유형이 다를 수 있다는 것을 생각해보아야 합니다.
모델링이 되었다면, 서비스에 대한 구성요소를 고려해볼 수 있습니다.
사용자, 관리자(호텔 직원) CDN, 공개 API 게이트 웨이, 내부 API, 호텔 서비스, 요금 서비스, 예약 서비스, 결제 서비스, 호텔 관리 서비스
세 번째 단계: 상세 설계
상세 설계를 진행할 때는 시스템 안정성, 성능, 확장성, 그리고 데이터 일관성을 중점적으로 고려하여 문제를 분석하는 것이 중요합니다. 또한, 장기적인 확장성과 유지보수성을 함께 염두에 두어, 시스템이 성장할 수 있도록 적절한 해결책을 마련하는 것이 필수적입니다.
트러블 슈팅 과정에서는 문제의 빈도와 시스템에 미치는 영향을 기준으로 우선순위를 정해, 발생할 수 있는 문제를 효율적으로 해결해야 합니다. 이를 통해, 설계 단계에서부터 발생 가능한 문제를 미리 대비하고 시스템의 안정성을 높일 수 있습니다.
모델링 개선
호텔 객실 예약 시 특정 객실이 아닌 특정한 객실 유형을 예약하게 됩니다. 호텔의 모든 객실 유형을 담는 테이블에 대한 포인트 였습니다.
동시성 문제를 해결하는 방법
이중 예약이 발생할 수 있는 사례에 대해 두가지 예를 들어 접근해볼 수 있습니다.
동시성 문제를 접근하는 세 가지 방법론
시스템 규모 확장
예약 시스템이 단일 서비스가 아닌 플랫폼 형태로 확장을 한다면? 어떤 것들을 고려해야 할까?
객실 타입에 대한 확장성은 고려되어 있다는 것을 보장하고, 성능적인 부분만 고민해 보도록 합니다.
네 번째 단계: 마무리
호텔 예약 시스템 설계를 진행하며 요구사항을 수집하고, 규모를 추정하여 개략적인 시스템 설계를 완료하였습니다. 이후 API 설계, 데이터 모델링, 시스템 아키텍처 다이어그램을 통해 구체적인 방향을 잡아가던 중,
예약 처리 방식에 대한 관점이 잘못되었음을 확인
하였고, 이를 기반으로 데이터 스키마를 수정하는 과정을 거쳤습니다.서비스의 특성상
경쟁 조건(Race Condition)
이 발생할 수 있는 시나리오를 분석하며, 몇 가지 해결책을 제시하였습니다. 특히,비관적 락(Pessimistic Lock)
과낙관적 락(Optimistic Lock)
, 그리고데이터베이스 제약 조건
등을 사용한동시성 제어 방안
을 검토하였습니다.또한, 서비스 확장성을 고려하여 성능 향상을 위한 다양한 전략을 살펴보았습니다. 데이터베이스 샤딩과 레디스 캐시를 도입하여 확장성을 극대화할 수 있는 방안을 논의하였고, 시스템 규모 확장에 대한 준비를 철저히 했습니다.
마지막으로,
마이크로서비스 아키텍처(MSA)
에서 자주 발생할 수 있는 데이터 일관성 문제에 대해 논의하였으며, 이를 해결하기 위한 몇 가지 전략도 검토하였습니다. 이를 해결하기 위해2PC(Two-Phase Commit)
와사가 패턴(Saga Pattern)
을 검토하였습니다.결론적으로, 이번 호텔 예약 시스템 설계를 통해 확장성과 성능, 데이터 일관성을 고려한 시스템 설계를 달성했으며, 이러한 과정을 통해 안정적이고 효율적인 예약 시스템을 구현할 수 있음을 확인하였습니다.