ADR-10: 사용자별 공정성 미들웨어
| 날짜 | 작성자 | 레포지토리 |
|---|---|---|
| 2026-02-02 | @KubrickCode | worker |
배경
문제 상황
사용자별 제한 없이는 대량 요청 제출 시 큐 워커 독점, 불공정한 리소스 분배 발생:
| 시나리오 | 영향 |
|---|---|
| Free 사용자 10건 작업 제출 | 5개 워커 모두 점유; Pro 사용자 큐에서 대기 |
| 단일 사용자 대량 제출 | 다른 사용자 서비스 품질 저하 |
| 티어 구분 없음 | 유료 사용자가 무료 사용자 대비 우선권 없음 |
요구사항
| 요구사항 | 설명 |
|---|---|
| 사용자별 제한 | 전역이 아닌 사용자별 동시 작업 제한 |
| 티어 기반 할당량 | 상위 티어에 더 많은 동시 슬롯 제공 |
| 비파괴적 제한 | 작업 거부 아닌 지연; 모든 작업 최종 실행 |
| 낮은 오버헤드 | 작업 실행 지연 최소화 |
| 우아한 처리 | 스누즈된 작업 깨어날 때 thundering herd 방지 |
결정
River WorkerMiddleware를 통한 티어 기반 할당량의 사용자별 동시 작업 제한 구현.
티어별 제한
| 티어 | 동시 작업 수 |
|---|---|
| Free | 1 |
| Pro | 3 |
| Pro Plus | 3 |
| Enterprise | 5 |
아키텍처
┌─────────────────────────────────────────────────────────────────┐
│ Fairness Middleware 흐름 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 작업 수신 │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ UserJobExtractor │ 작업 args(JSON)에서 userID 추출 │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ TierResolver │ DB에서 사용자 구독 티어 조회 │
│ │ (DB 조회) │ 미확인 시 Free 기본값 │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ PerUserLimiter │ TryAcquire(userID, tier, jobID) │
│ │ (인메모리) │ │
│ └────────┬────────┘ │
│ │ │
│ ┌─────┴─────┐ │
│ │ │ │
│ acquired rejected │
│ │ │ │
│ ▼ ▼ │
│ 워커 JobSnooze(30s + jitter) │
│ 실행 재시도 반환 │
│ │ │
│ ▼ │
│ defer Release(userID, jobID) │
│ │
└─────────────────────────────────────────────────────────────────┘핵심 설계 결정
| 결정 | 근거 |
|---|---|
| Hook 대신 WorkerMiddleware | Hook은 JobSnooze 반환 불가; 미들웨어만 가능 |
| 인메모리 리미터 | 단일 배포 환경에서 인스턴스별 상태 허용 |
| job args 대신 DB에서 티어 | Web 레이어에 티어 미포함; 느슨한 결합 유지 |
| 30s + 10s jitter 스누즈 | 깨어날 때 thundering herd 방지 |
| 멱등 acquire/release | 동일 jobID 슬롯 중복 카운트 방지 |
| 시스템 작업 우회 | 빈 userID는 제한 우회 (스케줄 작업) |
고려한 옵션
옵션 A: 사용자별 WorkerMiddleware 제한 (선택)
인메모리에서 사용자별 동시 작업 수 추적하는 River WorkerMiddleware 구현. 작업 실행 전 티어 기반 제한 확인. 초과 시 JobSnooze 반환.
장점:
- 사용자별 정확한 동시성 제어
- 티어 기반 공정성 차별화
- 작업 거부 아닌 스누즈
- Jitter로 thundering herd 방지
단점:
- 인스턴스별 상태 (분산 아님)
- 티어 조회용 DB 쿼리
옵션 B: 단일 큐 우선순위 필드
티어 기반 우선순위 필드 추가. 높은 우선순위 먼저 처리.
기각: 우선순위는 순서에 영향, 동시성 아님. 단일 사용자 여전히 워커 독점.
옵션 C: 티어별 전용 워커 풀
티어별 전용 워커의 별도 큐.
기각: 풀 불균형 로드 시 리소스 비효율. 티어 내 단일 사용자 여전히 독점.
옵션 D: River Hook 접근
HookWorkBegin으로 제한 확인.
기각: Hook은 JobSnooze 반환 불가. 기술적 제약, 선호도 아님.
결과
긍정적:
- 사용자 간 공정한 리소스 분배
- 명확한 티어 가치 제안 (1 vs 3 vs 5 슬롯)
- 비파괴적 제한 (거부 아닌 스누즈)
- 기존 세마포어 패턴과 일관성 (ADR-06)
부정적:
- 인스턴스별 상태로 수평 확장 제한
- 티어 조회용 추가 DB 쿼리
- 제한 초과 사용자 30s+ 지연
운영:
- 티어별 스누즈 비율 모니터링
- 환경 변수로 티어 제한 노출
- 분산 리미터 향후 작업으로 문서화
설정
FAIRNESS_ENABLED=true
FAIRNESS_FREE_LIMIT=1
FAIRNESS_PRO_LIMIT=3
FAIRNESS_ENTERPRISE_LIMIT=5
FAIRNESS_SNOOZE_DURATION=30s
FAIRNESS_SNOOZE_JITTER=10s참조
- ADR-21: 할당량 예약 - 요청 수준 할당량 보호
- Worker ADR-06: 세마포어 클론 동시성 - 유사 인메모리 리미터 패턴
- River WorkerMiddleware 문서
- GitHub 커밋: 620849f, 527c1ae
