본문 바로가기
프로그램 (PHP,Python)

예약 몰림에 버티는 예약 API 점검: 중복·재시도·좌석 경쟁

by myview73188 2026. 10. 13.

예약 몰림에 버티는 예약 API 점검: 중복·재시도·좌석 경쟁

728x90

예방접종 예약 페이지가 열리는 순간 짧은 시간에 평소의 수십 배 요청이 몰리는 일은 드물지 않습니다. 이때 시스템이 느려지는 것보다 더 위험한 것은 같은 좌석이 두 사람에게 확정되거나, 응답이 늦은 클라이언트가 재시도를 거듭해 서버를 더 압박하는 상황입니다. 앞서 웹 워커와 DB 커넥션을 점검했다면, 이제는 예약 API 코드가 동시 요청을 어떻게 처리하는지 봐야 합니다.

이 글은 PHP와 Python으로 만든 예약 API를 기준으로 점검 순서를 정리합니다. 레이트리밋을 어느 계층에 걸어야 하는지, 좌석 선점을 트랜잭션과 잠금으로 어떻게 보장하는지, 중복 예약을 멱등성 키와 유니크 제약으로 어떻게 막는지, 재시도를 어디서 끊어야 하는지를 확인 항목과 함께 다룹니다. 코드를 고치기 전에 무엇을 기준으로 확인할지 먼저 정하는 데 초점을 둡니다.

쏠림 때 예약 API에 생기는 세 가지 문제

예약 API에서 쏠림이 만드는 문제는 크게 세 가지입니다. 첫째는 중복 예약, 즉 같은 사용자나 같은 좌석이 두 번 확정되는 문제입니다. 둘째는 재시도 폭주로, 타임아웃을 맞은 클라이언트가 같은 요청을 반복해 보내며 부하를 키우는 현상입니다. 셋째는 좌석 선점 경쟁으로, 재고 확인과 차감 사이에 다른 요청이 끼어들어 잔여 좌석이 음수가 되거나 한 좌석을 여럿이 잡는 경우입니다.

세 문제의 원인은 다르지만 공통점이 하나 있습니다. 모두 '확인하고 나서 쓰는' 순서에서 틈이 생긴다는 점입니다. 그래서 점검은 정상 동작 테스트보다 같은 요청을 동시에 여러 번 보냈을 때의 결과를 기준으로 해야 합니다.

가상의 예를 들어 보겠습니다. 한 회차의 잔여 좌석이 1개일 때 두 요청이 거의 동시에 들어와 둘 다 잔여 1을 읽고 각자 차감하면, 같은 좌석에 두 명이 확정됩니다. 한 명은 나중에 현장에서 거절되고, 고객센터 문의가 쌓이면서 장애가 드러납니다. 코드에서는 SELECT로 읽은 값을 애플리케이션이 판단한 뒤 UPDATE하는 두 단계 구조가 대개 원인입니다.

레이트리밋은 앞단과 앱, 두 곳에 건다

레이트리밋은 요청 수를 줄이는 장치이지 좌석을 지키는 장치가 아닙니다. 그래도 쏠림 상황에서 가장 먼저 효과를 내는 방어선이므로, 예약 엔드포인트만 따로 한도를 주는 것이 중요합니다. 일반 조회 API와 같은 버킷을 쓰면 조회 폭주가 예약 요청까지 밀어냅니다.

앞단(게이트웨이·웹서버)에서 볼 것

IP나 API 키 단위로 초당 요청 수를 제한하고, 초과분은 429 Too Many Requests와 Retry-After 헤더로 돌려주는지 확인합니다. 요청이 앱 서버에 닿기 전에 걸러야 웹 워커와 DB 커넥션이 보호됩니다. 다만 같은 공인 IP 뒤에 사용자가 많은 기관 환경에서는 IP 단위 한도가 정상 사용자를 막을 수 있으므로, 로그인 세션이나 예약 토큰 기준을 함께 둡니다.

앱(PHP·Python)에서 볼 것

PHP라면 Laravel의 RateLimiter나 Redis 카운터를, Python이라면 Django·FastAPI 미들웨어나 Redis의 INCR와 EXPIRE 조합을 쓸 수 있습니다. 확인할 것은 한도 키가 사용자 ID인지, 초과 시 로직이 실제로 중단되는지, 그리고 Redis가 장애일 때 기본 동작이 허용 쪽인지 차단 쪽인지입니다. 장애 시 허용으로 두면 쏠림 때 보호가 사라지므로, 이 정책을 문서로 남기고 테스트합니다.

좌석 선점은 조건부 갱신과 행 잠금으로 보장한다

좌석 선점의 핵심은 재고 확인과 차감을 하나의 원자적 연산으로 묶는 것입니다. 가장 단순한 방법은 UPDATE 문에 조건을 넣어 잔여 수가 1 이상일 때만 차감하고, 영향받은 행 수가 1인 경우만 성공으로 처리하는 것입니다. PHP PDO의 rowCount()나 Python 드라이버의 cursor.rowcount로 이 값을 확인합니다.

여러 좌석을 한 번에 묶거나 가격 계산을 함께 해야 하는 복합 로직이라면, 트랜잭션 안에서 SELECT ... FOR UPDATE로 행을 잠그는 방식을 씁니다. Python SQLAlchemy에서는 with_for_update(), Laravel에서는 lockForUpdate()가 같은 역할을 합니다. 잠금을 쥔 채 외부 API를 호출하면 대기가 길어지므로, 트랜잭션에는 차감과 기록만 담아 짧게 유지합니다.

확인 포인트는 두 가지입니다. 잠금 없이 읽은 값으로 판단하는 코드가 남아 있지 않은지, 그리고 선점에 만료 시간이 있는지입니다. 결제 단계에서 이탈한 사용자가 좌석을 무기한 쥐고 있으면 실제 가용 좌석이 0으로 보입니다. 임시 선점에는 TTL을 두고, 만료된 선점을 되돌리는 작업이 있는지 확인합니다.

중복 예약은 유니크 제약과 멱등성 키로 막는다

중복 예약을 애플리케이션 조건문에만 맡기면 경쟁 상태에서 뚫립니다. 같은 사용자가 같은 회차에 한 번만 예약하도록 DB에 (회차 ID, 사용자 ID) 복합 유니크 제약을 걸어야 최종 방어가 됩니다. 유니크 위반 예외를 잡아 '이미 예약됨' 응답으로 매핑하는 부분까지 확인합니다.

멱등성 키는 같은 요청의 재전송을 한 번의 처리로 묶는 장치입니다. 클라이언트가 Idempotency-Key 헤더에 요청마다 고유한 값을 담고, 서버는 키와 요청 본문의 해시, 처리 결과를 저장합니다. 같은 키가 다시 오면 저장된 응답을 그대로 돌려주고, 키는 같은데 본문 해시가 다르면 명확한 오류로 거절합니다.

구현할 때 주의할 점은 키 저장과 예약 처리가 같은 트랜잭션에 있어야 한다는 것입니다. 예약은 성공했는데 키 저장만 실패하면 재전송이 다시 예약을 만듭니다. 키의 보관 기간도 정책으로 정하고, 저장된 응답에 개인정보가 그대로 남는지도 ISMS-P 관점에서 점검 대상으로 삼습니다.

재시도 폭주를 끊는 규칙과 적용 지점

재시도는 장애를 가장 쉽게 키우는 기능입니다. 클라이언트가 타임아웃마다 즉시 다시 보내면 서버 부하는 요청 수에 재시도 횟수를 곱한 만큼 늘어납니다. 재시도에는 지수 백오프와 지터를 함께 적용해 간격을 흩뜨리고, 최대 횟수를 정해 두는 것이 기본입니다.

재시도 가능 여부를 HTTP 메서드만으로 판단하지는 않습니다. 예약 생성은 POST이므로 기본적으로는 재시도하면 안 되지만, 멱등성 키가 있으면 안전하게 재시도할 수 있습니다. 반대로 키 없이 서버가 내부 호출을 자동으로 재시도하는 구조는 중복 예약의 흔한 원인입니다. 아래 표로 항목별 기준을 맞춰 보세요.

점검 항목 막는 문제 적용 지점 확인 방법
레이트리밋 요청 폭주 게이트웨이, 앱 미들웨어 429와 Retry-After 응답 확인
행 잠금·조건부 UPDATE 좌석 중복 차감 리포지토리, 트랜잭션 경계 동시 요청으로 잔여 수 확인
복합 유니크 제약 같은 사용자 중복 예약 DB 스키마 중복 INSERT 시 예외 매핑 확인
멱등성 키 재전송에 의한 중복 처리 API 입구, 키 저장소 같은 키 재요청 시 같은 응답인지 확인
임시 선점 TTL 좌석 장기 점유 선점 테이블, 배치 작업 만료 후 잔여 수 복구 확인
재시도 정책 재시도에 의한 부하 증폭 클라이언트 SDK, 서버 간 호출 백오프 간격과 최대 횟수 로그 확인

바로 해 볼 순서

  1. 예약 엔드포인트에만 부하를 주고, 같은 회차에 동시 요청 50건을 보냈을 때 잔여 수와 확정 건수가 맞는지 확인합니다.
  2. 예약 생성 경로에서 SELECT 뒤에 UPDATE하는 구조를 찾고, 조건부 UPDATE나 행 잠금으로 바꿀 수 있는지 판단합니다.
  3. 회차와 사용자 조합에 복합 유니크 제약이 있는지 스키마에서 확인하고, 없으면 기존 중복 데이터부터 정리합니다.
  4. 레이트리밋 키, 초과 응답 형식, Redis 장애 시 동작을 문서와 테스트로 확인합니다.
  5. 클라이언트의 재시도 설정과 멱등성 키 발급 로직을 확인하고, 재시도 로그에 시도 횟수와 키 해시를 남깁니다.

자주 묻는 질문

레이트리밋만 걸면 중복 예약은 막히나요?

아닙니다. 레이트리밋은 요청 수를 줄일 뿐이고, 동시에 들어온 두 요청이 같은 잔여 좌석을 읽는 것을 막지 못합니다. 중복 방지는 조건부 갱신, 행 잠금, 유니크 제약 같은 DB 수준의 장치가 맡아야 합니다.

멱등성 키는 얼마나 보관하면 되나요?

보관 기간은 클라이언트가 재시도하는 가장 긴 시간을 기준으로 정합니다. 다만 구체적인 기간은 팀의 장애 대응 기준과 개인정보 보관 정책을 함께 검토해 정하는 것이 맞습니다.

쏠림 때 대기열을 두는 것이 나은가요?

대기열은 좌석 경쟁을 줄이는 데 효과적이지만, 대기 순번을 발급하는 API가 새로 생기므로 같은 점검을 거쳐야 합니다. 먼저 현재 API가 동시 요청에서 정확한지 확인하고, 그다음에 대기열 도입을 검토하는 순서를 권합니다.

정리

  • 중복 예약은 애플리케이션 조건문이 아니라 조건부 갱신, 행 잠금, 유니크 제약으로 막습니다.
  • 재시도는 백오프와 상한을 두고, 생성 요청은 멱등성 키와 함께 허용합니다.
  • 레이트리밋은 예약 경로를 분리해 앞단과 앱 양쪽에 걸고, 장애 시 동작을 정책으로 남깁니다.

오늘 할 일: 예약 생성 경로에서 SELECT 뒤 UPDATE로 좌석을 차감하는 코드를 찾고, 같은 회차에 동시 요청을 보내 잔여 수가 맞는지 한 번 확인해 보세요.

728x90

댓글