ADR-21: 동시 요청 처리용 할당량 예약
| 날짜 | 작성자 | 리포지토리 |
|---|---|---|
| 2026-02-01 | @specvital | web, worker, infra |
배경
이벤트 기반 과금의 동시성 갭
ADR-13 (빌링 및 쿼터 아키텍처)에서 작업 완료 시점에 사용량 이벤트를 기록하는 이벤트 기반 사용량 추적 방식 수립. 이 모델은 과금 정확성 보장(성공한 작업만 과금) 및 캐시 우선 아키텍처와의 정합성 확보(캐시 히트 시 이벤트 미생성).
그러나 쿼터 검사(요청 제출 시점)와 쿼터 소비(작업 완료 시점) 사이에 시간적 갭 존재. 높은 동시성 상황에서 이 갭이 경쟁 조건(race condition) 유발:
경쟁 조건 시나리오:
| 시점 | 동작 | 사용자 상태 |
|---|---|---|
| T0 | 사용자 4998/5000 사용 중 | - |
| T1 | 요청 A 쿼터 검사 | 4998 < 5000 → 통과 |
| T2 | 요청 B 쿼터 검사 | 4998 < 5000 → 통과 |
| T3 | 요청 A 완료 | 5008 사용 (한도 초과) |
| T4 | 요청 B 완료 | 5018 사용 |
미해결 시 문제점:
- 초과 쿼터 작업 처리로 서버 리소스 낭비
- 실제 사용량이 한도 초과하는 과금 불일치
- 요청 타이밍에 따른 불공정한 시스템 동작
제약 조건
| 제약 | 근거 |
|---|---|
| Web 레벨 차단 필수 | Worker 재검사 시 큐 및 컴퓨팅 리소스 낭비 |
| PostgreSQL 전용 솔루션 | ADR-04 (River는 PostgreSQL 사용)와 정합성 |
| 사용자에게 보이는 과금 이상 없음 | 과금 정확성에 대한 사용자 신뢰 유지 |
| Worker가 라이프사이클 소유 | ADR-12에서 Worker를 레코드 생성자로 규정 |
결정
동시 요청 처리를 위한 쿼터 예약 패턴과 원자적 트랜잭션 채택.
예약 메커니즘
진행 중인 쿼터 예약을 추적하는 quota_reservations 테이블 도입:
| 컬럼 | 타입 | 목적 |
|---|---|---|
| id | uuid (PK) | 기본 키 |
| user_id | uuid (FK) | 사용자 계정 연결 |
| event_type | usage_event_type | specview 또는 analysis |
| reserved_amount | int | 예상 쿼터 소비량 |
| job_id | bigint (UNIQUE) | River 작업 연결 (정리용) |
| expires_at | timestamptz | 고아 정리용 1시간 TTL |
쿼터 검사 공식
sql
used + reserved + requested_amount <= limit구성 요소:
used: 현재 과금 기간의 usage_events 합계reserved: 활성 예약 합계 (expires_at > NOW())requested_amount: 현재 요청에 필요한 단위
트랜잭션 원자성
Web 레이어에서 단일 PostgreSQL 트랜잭션 내 실행:
- 쿼터 검사 (활성 예약 포함)
- 예약 레코드 생성
- River 큐에 작업 삽입 (InsertTx)
원자성 보장: 작업 삽입 실패 시 예약도 생성되지 않음 (롤백).
예약 라이프사이클
Web: 쿼터 검사 → 예약 생성 → InsertTx
↓
Worker: 작업 처리 → 예약 삭제 (성공 또는 실패)
↓
Cleanup: 1시간 후 고아 예약 만료검토된 옵션
옵션 A: 예약 패턴 (선택됨)
메커니즘: 작업 삽입과 원자적으로 예약 생성; Worker가 완료 시 삭제.
| 측면 | 평가 |
|---|---|
| 쿼터 정확도 | 보장 - 동시 초과 예약 불가 |
| 사용자 경험 | 투명 - 예약은 내부 상태 |
| 리소스 효율 | 높음 - Web에서 초과 쿼터 차단 |
| 복잡도 | 중간 - 추가 테이블 및 정리 작업 |
옵션 B: 선차감 후 환불
메커니즘: 제출 시 쿼터 차감; 작업 실패 시 환불.
| 측면 | 평가 |
|---|---|
| 쿼터 정확도 | 보장 |
| 사용자 경험 | 나쁨 - 처리 중 부풀려진 사용량 표시 |
| 리소스 효율 | 높음 |
| 복잡도 | 높음 - 다양한 실패 상태별 환불 로직 필요 |
거부 사유: 과금 불안과 지원 부담 유발. 작업 처리 중 대시보드를 보는 사용자에게 환불될 수 있는 사용량이 표시되어 신뢰 저하.
옵션 C: Worker 재검사
메커니즘: Web에서 낙관적 검사; Worker에서 권위적 검사.
| 측면 | 평가 |
|---|---|
| 쿼터 정확도 | 보장 (Worker 레벨) |
| 사용자 경험 | 나쁨 - 작업 수락 후 비동기 거부 |
| 리소스 효율 | 나쁨 - 초과 쿼터 작업이 큐 용량 소비 |
| 복잡도 | 낮음 |
거부 사유: Web 레벨 차단 요구 사항 위반. 거부될 작업에 큐 및 Worker 리소스 낭비.
결과
긍정적
| 이점 | 영향 |
|---|---|
| 정확한 쿼터 집행 | 동시 요청이 한도 초과 불가 |
| 리소스 효율 | 초과 쿼터 작업에 Worker 사이클 낭비 없음 |
| 사용자 공정성 | 완료된 작업만 과금에 표시 |
| 원자적 일관성 | 작업 + 예약이 단일 트랜잭션 |
| 깔끔한 실패 처리 | 작업 결과와 관계없이 예약 삭제 |
부정적
| 트레이드오프 | 완화 방안 |
|---|---|
| 추가 스키마 복잡도 | 표준 패턴; 명확한 목적의 단일 테이블 |
| 예약 집계 쿼리 오버헤드 | (user_id, event_type, expires_at) 인덱스 |
| 고아 정리 필요 | 구성 가능한 1시간 TTL의 스케줄 작업 |
| 디버깅 복잡도 | 예약/해제 지점에 구조화된 로깅 |
기술적 영향
| 측면 | 요구 사항 |
|---|---|
| 트랜잭션 범위 | InsertTx와 CreateReservation이 PostgreSQL 트랜잭션 공유 |
| 인덱스 설계 | 효율적인 예약 조회 및 만료를 위한 복합 인덱스 |
| 정리 스케줄 | expires_at < NOW() 조건의 예약 삭제 크론 작업 |
| 모니터링 | 고아 수 알림 (Worker 상태 문제 표시) |
| Worker 수정 | 작업 완료 시 job_id로 예약 삭제 |
참조
- ADR-13: 빌링 및 쿼터 아키텍처
- ADR-04: 큐 기반 비동기 처리
- ADR-12: Worker 중심 분석 라이프사이클
- GitHub Issue #291: Quota Reservation System
- 커밋
d3f15bf: feat(db): add quota_reservations table for concurrent request handling
