-
이코에코(Eco²) Eventual Consistency #3: ext-authz 로컬 캐시 일관성 보장 설계이코에코(Eco²)/Eventual Consistency 2025. 12. 30. 15:12

1. 문제 정의
1.1 현재 아키텍처에서 발생할 수 있는 불일치
┌─────────────────────────────────────────────────────────────────────────────┐ │ 현재 아키텍처 (Best Effort) │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ User Logout │ │ │ │ │ ▼ │ │ ┌─────────────────┐ ┌─────────────────┐ ┌───────────────┐ │ │ │ auth-api │ ──────► │ RabbitMQ │ ──────► │ ext-authz │ │ │ │ (블랙리스트 │ │ (blacklist. │ │ (local cache) │ │ │ │ 등록 + 발행) │ │ events) │ │ │ │ │ └─────────────────┘ └─────────────────┘ └───────────────┘ │ │ │ │ │ │ │ │ ❌ 실패 시 │ │ │ │ │ ▼ │ │ ┌──────────────┐ │ │ │ 메시지 손실 │ │ │ │ (ext-authz │ │ │ │ 캐시 미갱신) │ │ │ └──────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘1.2 불일치 시나리오
보안 취약점 MQ 발행 실패 블랙리스트 토큰이 JWT 만료까지 계속 유효 불일치 지속 시간 JWT 만료까지 최대 1시간 (access_token TTL) 탐지 불가 로그 확인 필요 실시간 모니터링 어려움 1.3 Eventual Consistency의 범위
ext-authz 로컬 캐시는 Eventual Consistency 모델을 따르지만, 불일치의 원인과 범위를 명확히 구분해야 합니다.
┌─────────────────────────────────────────────────────────────────────────────┐ │ Eventual Consistency 발생 지점 │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ 1. 정상적인 전파 지연 (허용) │ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ auth-api → RabbitMQ → ext-authz │ │ │ │ │ │ │ │ 지연 시간: ~10-50ms │ │ │ │ 영향: 무시 가능 │ │ │ └────────────────────────────────────────────────────────────┘ │ │ │ │ 2. MQ 발행 실패 (문제) ◄── 이번 설계의 해결 대상 │ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ auth-api → ❌ RabbitMQ │ │ │ │ │ │ │ │ 불일치 지속: JWT 만료까지 (최대 1시간) │ │ │ │ 영향: 보안 취약점 │ │ │ └────────────────────────────────────────────────────────────┘ │ │ │ │ 3. ext-authz Pod 재시작 (별도 처리 완료) │ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ 새 Pod는 Redis에서 Bootstrap │ │ │ │ │ │ │ │ 지연 시간: Pod 시작 시 1회 │ │ │ │ 영향: 없음 (이미 구현됨) │ │ │ └────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘
2. 검토한 선택지
2.1 Option A: Redis Fallback Lookup
개념: 로컬 캐시에 없으면 Redis에서 조회
ext-authz 요청 처리: 1. Local Cache에서 JTI 확인 2. Cache Miss → Redis 조회 (Fallback) 3. Redis에서 발견 → 블랙리스트 4. Redis에도 없음 → 정상 토큰장점:
- 구현 간단 (Relay Worker 불필요)
- 항상 최신 상태 보장
❌ 반려: 로컬 캐시의 의미 상실
┌─────────────────────────────────────────────────────────────────────────────┐ │ Redis Fallback의 근본적 문제 │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ 블랙리스트 토큰 비율 (실제 운영 환경) │ │ ┌────────────────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ 전체 요청 중 블랙리스트 토큰: ~0.01% 미만 │ │ │ │ │ │ │ │ ├── 정상 토큰: 99.99%+ ────────────────────────────────────┐ │ │ │ │ │ │ │ │ │ │ │ 모든 요청이 Cache Miss → Redis 조회 │ │ │ │ │ │ │ │ │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ └── 블랙리스트 토큰: ~0.01% ─────┐ │ │ │ │ │ │ │ │ │ 로컬 캐시 Hit → Redis 불필요 │ │ │ │ │ │ │ │ │ └────────────────────────────────────┘───────────────────────────┘ │ │ │ │ 결과: │ │ - 99.99%의 요청이 Redis를 조회함 │ │ - 로컬 캐시의 이점 완전 상실 │ │ - ext-authz 지연 시간: ~0.05ms → ~2-5ms (40-100x 증가) │ │ - Redis가 ext-authz의 병목이 됨 │ │ │ └─────────────────────────────────────────────────────────────────────────────┘결론: 로컬 캐시를 도입한 목적 자체가 무효화됨. 불채택
2.2 Option B: Negative Cache
개념: "블랙리스트에 없음"도 캐싱
ext-authz 요청 처리: 1. Local Cache 확인 - "blacklisted" → 차단 - "not_blacklisted" → 통과 (캐시된 부재 정보) - Cache Miss → Redis 조회 후 결과 캐싱장점:
- Redis Fallback의 Cache Miss 문제 해결
- 한 번 조회 후 캐싱
❌ 반려: 로컬 메모리 리소스 과소비
┌─────────────────────────────────────────────────────────────────────────────┐ │ Negative Cache의 메모리 문제 │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ 시나리오: 1,000명의 활성 사용자, 각 사용자 평균 10개 토큰 │ │ │ │ 블랙리스트만 캐싱: │ │ ├── 로그아웃 비율: ~5%/일 │ │ ├── 블랙리스트 토큰: ~500개 │ │ └── 메모리: ~50KB │ │ │ │ Negative Cache 포함: │ │ ├── 모든 정상 토큰도 캐싱: ~9,500개 │ │ ├── 메모리: ~1MB │ │ └── 문제: 캐시 만료 전 새 토큰 발급 시 무한 증가 │ │ │ │ 대규모 시스템: │ │ ├── 100,000 활성 사용자 × 10 토큰 = 1,000,000 엔트리 │ │ └── 메모리: ~100MB+ (ext-authz Pod당) │ │ │ │ 추가 복잡성: │ │ - Negative Cache 만료 정책 필요 │ │ - 새 토큰 발급 시 Negative Cache 무효화 필요 │ │ - 결국 전체 토큰 저장소가 됨 (설계 의도 벗어남) │ │ │ └─────────────────────────────────────────────────────────────────────────────┘결론: 복잡도 대비 이점 없음. 블랙리스트만 캐싱하는 현재 설계가 더 효율적. 불채택
2.3 Option C: 즉시 재시도 (Immediate Retry)
개념: MQ 발행 실패 시 즉시 재시도
def publish_add(self, jti: str, expires_at: datetime) -> bool: for attempt in range(3): try: self._publish(event) return True except Exception: time.sleep(0.1 * (attempt + 1)) # Exponential backoff return False # 3회 실패 시 메시지 손실장점:
- 구현 간단
- 대부분의 일시적 오류 해결
❌ 문제점
UX 지연 로그아웃 응답 시간 300ms+ 증가 완전한 해결 불가 3회 실패 시 여전히 메시지 손실 장애 상황 악화 MQ 장애 시 모든 로그아웃 요청 지연
결론: 단독으로 사용 불가. 보조 수단으로만 적합. 부분 채택
2.4 Option D: Observability 기반 재발행
개념: 실패 로그 기반 수동/자동 재발행
1. MQ 발행 실패 시 로그 기록 (error level) 2. 알림 발송 (Slack, PagerDuty) 3. 운영자가 수동으로 재발행 스크립트 실행 또는 CronJob이 실패 로그 파싱 후 재발행장점:
- 구현 간단
- 실패 가시성 확보
❌ 문제점:
수동 개입 필요 운영 부담 증가 지연 시간 알림 → 확인 → 조치까지 수 분~수 시간 로그 파싱 복잡 CronJob 구현 시 로그 포맷 의존 원자성 부재 재발행 중 중복 발생 가능
결론: 휴먼 에러 위험성 내재, 스크립트에 의존해 정합성을 해칠 우려 다분
2.5 Option E: Outbox Pattern ✅
개념: 실패한 이벤트를 Redis Outbox에 저장, Relay Worker가 백그라운드 재발행
┌─────────────────────────────────────────────────────────────────────────────┐ │ Outbox Pattern 아키텍처 │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ User Logout │ │ │ │ │ ▼ │ │ ┌─────────────────┐ │ │ │ auth-api │ │ │ │ (blacklist_ │ │ │ │ publisher) │ │ │ └────────┬────────┘ │ │ │ │ │ ▼ │ │ ┌────────────────────────────────┐ │ │ │ RabbitMQ 발행 시도 (1차) │ │ │ └────────────────┬───────────────┘ │ │ │ │ │ ┌───────────┴───────────┐ │ │ │ │ │ │ ▼ 성공 (~99%) ▼ 실패 (~1%) │ │ ┌─────────────┐ ┌─────────────────┐ │ │ │ RabbitMQ │ │ Redis Outbox │ │ │ │ (blacklist. │ │ (outbox: │ │ │ │ events) │ │ blacklist) │ │ │ └──────┬──────┘ └────────┬────────┘ │ │ │ │ │ │ │ ▼ │ │ │ ┌─────────────────┐ │ │ │ │ auth-relay │ ◄── 1초마다 폴링 │ │ │ │ (Relay Worker) │ │ │ │ └────────┬────────┘ │ │ │ │ │ │ │ ▼ 재발행 │ │ │ ┌─────────────────┐ │ │ └──────────────►│ RabbitMQ │ │ │ │ (blacklist. │ │ │ │ events) │ │ │ └────────┬────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────┐ │ │ │ ext-authz │ │ │ │ (local cache) │ │ │ └─────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘장점:
정상 경로 무영향 99%+ 요청은 기존과 동일하게 처리 UX 무영향 실패해도 로그아웃 응답 즉시 반환 At-Least-Once 메시지 손실 없음 보장 복구 가능 DLQ로 문제 이벤트 분리 확장 가능 Relay Worker 수평 확장 가능 단점:
추가 컴포넌트 Relay Worker 운영 필요 경량 구현 (~50MB 이미지) 지연 최대 1초 추가 지연 폴링 주기 조절 가능 복잡도 DLQ 모니터링 필요 알림 설정
3. 선택지 비교 요약
Redis Fallback 없음 낮음 100% ❌ 무효화 불채택 Negative Cache 없음 중간 100% △ 복잡 불채택 즉시 재시도 300ms+ 낮음 ~95% ✅ 부분 채택 Observability 없음 낮음 수동 ✅ 불채택 Outbox Pattern 없음 중간 ~100% ✅ ✅ 채택
4. Redis Fallback을 포기한 핵심 이유
4.1 로컬 캐시 도입 목적 회고
ext-authz에 로컬 캐시를 도입한 이유:
┌─────────────────────────────────────────────────────────────────────────────┐ │ 로컬 캐시 도입 목적 │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ Before (Redis 직접 연결): │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ ext-authz → Redis (매 요청마다) │ │ │ │ │ │ │ │ 지연: ~2-5ms │ │ │ │ 병목: Redis 연결 수, 네트워크 │ │ │ │ 확장성: Redis가 SPOF │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ After (로컬 캐시): │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ ext-authz → Local Cache (Go sync.Map) │ │ │ │ │ │ │ │ 지연: ~0.01-0.05ms (40-200x 개선) │ │ │ │ 병목: 없음 │ │ │ │ 확장성: 무제한 (각 Pod 독립) │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ 핵심 가치: │ │ - 인증 지연 시간 최소화 │ │ - Redis 부하 제거 │ │ - ext-authz 수평 확장 자유도 │ │ │ └─────────────────────────────────────────────────────────────────────────────┘4.2 Redis Fallback이 목적을 훼손하는 이유
┌─────────────────────────────────────────────────────────────────────────────┐ │ Redis Fallback 적용 시 결과 │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ 요청 처리 흐름: │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ 토큰 검증 요청 │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ Local Cache 조회 │ │ │ │ │ │ │ │ │ ├── Hit (블랙리스트) → 차단 (~0.01ms) │ │ │ │ │ 발생 빈도: ~0.01% │ │ │ │ │ │ │ │ │ └── Miss (정상 토큰) → Redis Fallback (~2-5ms) │ │ │ │ 발생 빈도: ~99.99% ◄── 문제 │ │ │ │ │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ 결과: │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ 평균 지연 = 0.01% × 0.01ms + 99.99% × 3ms = ~3ms │ │ │ │ │ │ │ │ → 로컬 캐시 없을 때와 동일! │ │ │ │ → 로컬 캐시 도입 이유가 사라짐 │ │ │ │ │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ 비유: │ │ "집에 냉장고를 샀는데, 음식이 있는지 확인할 때마다 │ │ 마트에 가서 확인한다" │ │ │ └─────────────────────────────────────────────────────────────────────────────┘4.3 핵심 아이디어
블랙리스트는 예외다. 대부분의 토큰(99.99%+)은 블랙리스트에 없다.
따라서 "없음"을 확인하기 위해 매번 Redis를 조회하면, 로컬 캐시의 존재 이유가 사라진다.Outbox Pattern은 이 문제를 회피한다.
- 정상 경로(99%+)는 로컬 캐시만 사용
- MQ 발행 실패(1% 미만)만 Outbox로 처리
- 로컬 캐시의 성능 이점 100% 유지
5. 최종 설계: Outbox Pattern + Retry
5.1 채택한 전략
┌─────────────────────────────────────────────────────────────────────────────┐ │ 하이브리드 전략 │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ 1차 방어: RabbitMQ 직접 발행 │ │ ├── 성공률: ~99%+ │ │ └── 지연: ~10-50ms │ │ │ │ 2차 방어: Redis Outbox 적재 (발행 실패 시) │ │ ├── 적재 성공률: ~99.9%+ (Redis는 MQ보다 안정적) │ │ └── Relay Worker가 백그라운드 재발행 │ │ │ │ 3차 방어: DLQ (Dead Letter Queue) │ │ ├── Relay 재발행도 실패 시 DLQ로 이동 │ │ └── 수동 처리 또는 알림 │ │ │ │ 메시지 손실 확률: │ │ = MQ 실패 × Redis 실패 × Relay 실패 │ │ = 0.01 × 0.001 × 0.001 │ │ = 0.00000001% (사실상 0) │ │ │ └─────────────────────────────────────────────────────────────────────────────┘5.2 구현 원칙
Non-blocking 로그아웃 응답에 재시도/Outbox 적재 시간 포함 안 함 FIFO Outbox는 LPUSH, Relay는 RPOP으로 순서 보장 At-Least-Once 중복 가능하나 손실 없음 (ext-authz에서 중복 허용) Graceful Degradation Relay 장애 시에도 auth-api 정상 동작 5.3 Redis 키 설계
Key Type 설명 TTL outbox:blacklistList 실패한 이벤트 (FIFO) 영구 outbox:blacklist:dlqList 재발행도 실패한 이벤트 영구
6. 트레이드오프 수용
6.1 수용한 트레이드오프
추가 컴포넌트 (Relay Worker) 경량 (~50MB), 단순 로직, 기존 노드에 배치 최대 1초 추가 지연 MQ 장애 시에만 발생, 정상 시 0 At-Least-Once (중복 가능) ext-authz가 중복 이벤트 허용 (idempotent) 6.2 거부한 트레이드오프
Redis Fallback 로컬 캐시 무효화, 성능 40-200x 저하 Negative Cache 메모리 폭발, 복잡도 증가 수동 개입 운영 부담, 지연 시간 보장 불가
7. 결론
7.1 설계 결정 요약
┌─────────────────────────────────────────────────────────────────────────────┐ │ 설계 결정 요약 │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ 문제: MQ 발행 실패 시 ext-authz 로컬 캐시 불일치 (보안 취약점) │ │ │ │ 검토한 선택지: │ │ ├── Redis Fallback → 불채택 (로컬 캐시 무효화) │ │ ├── Negative Cache → 불채택 (메모리 폭발) │ │ ├── 즉시 재시도 → 부분 채택 (UX 영향) │ │ ├── Observability → 불채택 (수동 개입) │ │ └── Outbox Pattern → ✅ 채택 │ │ │ │ 선택 이유: │ │ ├── 정상 경로 무영향 (99%+) │ │ ├── 로컬 캐시 성능 이점 유지 │ │ ├── At-Least-Once 메시지 보장 │ │ └── 복구 가능 (DLQ) │ │ │ └─────────────────────────────────────────────────────────────────────────────┘7.2 기대 효과
MQ 발행 실패 시 메시지 손실 100% ~0% 로컬 캐시 성능 (p99) ~0.05ms ~0.05ms (유지) 운영 부담 수동 확인 자동 복구 + DLQ 알림 보안 취약점 지속 시간 JWT 만료까지 (최대 1시간) ~1초 '이코에코(Eco²) > Eventual Consistency' 카테고리의 다른 글
이코에코(Eco²) Eventual Consistency #4: Blacklist Relay Worker 구현 (0) 2025.12.30 이코에코(Eco²) Eventual Consistency #2: ext-authz 2500 VUs 부하 테스트 (0) 2025.12.30 이코에코(Eco²) Eventual Consistency #1: ext-authz Blacklist 로컬 캐시 및 Fanout 구현 (0) 2025.12.30 이코에코(Eco²) Eventual Consistency #0: ext-authz 로컬 캐싱 설계 (0) 2025.12.29