속도 제한¶
Continuum Router는 남용을 방지하고, 리소스를 공정하게 배분하며, 백엔드 서비스를 과부하로부터 보호하기 위해 요청 속도를 제한합니다. 속도 제한 시스템은 다중 계층 보호와 함께 토큰 버킷 알고리즘을 사용합니다.
개요¶
라우터는 다중 계층 속도 제한을 구현합니다:
- 클라이언트별 제한: 개별 클라이언트가 시스템을 압도하는 것을 방지
- 백엔드별 제한: 개별 백엔드 서비스를 과부하로부터 보호
- API 키별 제한: 인증된 키 단위로 제한을 적용하고, 키별 오버라이드 지원
- 모델별 제한: 특정 (고비용) 모델 보호
- 전역 제한: 전체 시스템 안정성 보장
제한이 적용되는 위치¶
각 차원은 자신이 키로 쓰는 식별 정보가 이미 존재하는 가장 바깥 지점에 장착되며, 정확히 한 곳에서만 집행됩니다.
- 전역 (
limits.global): 인증과 CORS보다 바깥에 있는 애플리케이션 전체 장착 지점 하나가 집행합니다. 모든 요청이 정확히 한 번 차감됩니다. 인증이 나중에401로 거절하는 API 요청(그래서 API 키 추측이 계량됩니다), 실패한 admin 인증을 포함한 admin 요청,/status,/version, WebUI, Files, 메트릭, CORS 프리플라이트,404폴백이 모두 포함됩니다. - 클라이언트별 (
limits.per_client): API 라우트(/v1/*,/anthropic/*,/embed_sparse)에 인증보다 바깥으로 장착됩니다. 이 차원은 피어 주소를 키로 쓰는데, 그 주소는 인증이 실행되기 전에 연결이 제공하므로, 주소 하나에서 오는 미인증 폭주를 공유 전역 버킷에 떠넘기지 않고 직접 묶을 수 있습니다. - 식별 차원 (
per_api_key,per_model,per_backend): API 라우트에 인증보다 안쪽으로 장착됩니다. 이 차원들이 필요로 하는AuthContext가 그 지점에 존재하기 때문입니다. 이 장착 지점은 전역 버킷과 클라이언트별 버킷을 건드리지 않습니다.
따라서 API 라우트의 요청 경로는 전역 -> 클라이언트별 -> 인증 -> 키별 -> 핸들러 순입니다. 장착 지점들이 서로 겹치지 않는 차원을 집행하므로, 어떤 경로를 지나든 요청 하나가 같은 버킷에서 두 번 차감되는 일은 없습니다.
전역 장착 지점은 /health와 /healthz 두 경로를 조건 없이 예외 처리합니다. 포화된 라우터도 자기 활성 상태 검사에는 답해야 하며, 그러지 못하면 오케스트레이터가 과부하를 흘려보내는 도중에 프로세스를 재시작해 버립니다. 두 경로 모두 고정된 본문을 돌려주므로 예외로 두는 비용이 없습니다.
메트릭 엔드포인트도 예외지만, 그 예외는 고정된 경로가 아니라 설정에서 유도됩니다. 실제 metrics.path를 따르고 metrics.enabled가 참일 때만 적용됩니다. 그래서 메트릭을 끈 라우터에서 /metrics는 다른 매칭되지 않은 경로와 똑같이 계량되고, 사용자 지정 metrics.path로 옮긴 엔드포인트는 예외를 유지합니다. 예전에는 반대여서, 경로를 옮겨 둔 운영자가 정작 필요한 장애 시점에 모니터링을 차단당했습니다.
스크레이프를 차단 대상에서 빼는 것은 의도한 맞교환입니다. 활성화된 메트릭 엔드포인트 스크레이프는 백엔드 게이지를 다시 만들고 레지스트리 전체를 직렬화하므로 메트릭 카디널리티에 비례해 비용이 커지고, 리미터는 그 폭주를 막아 주지 않습니다. metrics.auth와 네트워크 정책으로 메트릭 엔드포인트를 보호하십시오. metrics.auth를 설정하지 않으면 인증 없이 열려 있고, 예외 처리까지 겹치면 바깥에서 라우터에 부하를 거는 가장 값싼 수단이 됩니다.
기억해 둘 결과가 하나 있습니다. 인증되지 않았거나 라우팅되지 않는 트래픽(401로 거절된 요청, 404 폴백에 닿는 쓰레기 경로)도 API 트래픽과 같은 limits.global 버킷을 소비합니다. 그런 트래픽의 폭주는 묶이지만, 같은 버킷을 쓰는 API 요청이 함께 차단될 수도 있습니다. limits.global을 예상 API 속도보다 여유 있게 잡거나, 라우터 앞단에서 그런 트래픽을 먼저 버리는 프록시를 두십시오. API 경로에 한해서는 per_client가 이 범위를 좁혀 줍니다. 주소 하나에서 오는 폭주는 공유 버킷을 마르게 하기 전에 그 주소의 버킷에서 먼저 차단되기 때문입니다. 다만 API가 아닌 경로의 트래픽은 여전히 limits.global만으로 묶입니다.
헤더 귀속과 거절 우선순위¶
하나의 요청에 여러 장착 지점이 의견을 낼 수 있으므로, 클라이언트가 보는 값은 다음 규칙으로 정해집니다.
- 성공 시
x-ratelimit-*헤더는 인증된 식별 정보에 묶인 할당량만 보고합니다. 현재로서는per_api_key입니다. 호출자는 그 버킷의 키가 되는 API 키를 가지고 있음을 이미 증명했으므로, 이 값은 본인이 이미 가진 정보 외에는 알려 주지 않습니다. 키별 차원이 요청을 평가하지 않았다면 응답에x-ratelimit-*가 아예 실리지 않습니다. - 공유 버킷은 허용된 응답에 절대 보고하지 않습니다.
limits.global,per_model,per_backend는 플릿 전체가 함께 소비하고,per_client는 같은 출발 주소 뒤에 있는 모두(NAT, 이그레스 게이트웨이, 사내 프록시)가 함께 소비하므로, 어느 것도 호출자 본인의 할당량이 아닙니다.per_client는 인증보다 먼저 평가되기까지 하므로, 라우터가 누구에게 보고하는지조차 알 수 없는 시점입니다. 네 차원 모두 집행은 설정 그대로 이루어지며, 다만 광고하지 않습니다. 클라이언트에게 할당량 헤더를 보여 주고 싶다면per_api_key를 설정하십시오. - 거절 시에는 거절한 차원의 값이 기준입니다. 공유 차원이라도 마찬가지로
error.details.limit_type,retry-after,x-ratelimit-*가 모두 요청을 차단한 차원을 설명합니다. 클라이언트에게 재시도 힌트가 필요하고, "버킷이 비었다"는 사실은429자체가 이미 말해 주기 때문입니다. 다른 장착 지점이 자기 값으로 덮어쓰지 않습니다. 전역 차원과 다른 차원이 동시에 거절할 상황이면 클라이언트는 전역 거절을 봅니다. 요청 경로에서 전역 장착 지점이 먼저 실행되기 때문입니다.Retry-After는 모든 경로에서 1 이상입니다. - 어느 장착 지점이 거절했든 브라우저에서 읽을 수 있습니다. CORS를 켜면
retry-after와x-ratelimit-*헤더가 항상Access-Control-Expose-Headers에 추가되므로 교차 출처 스크립트가 상태 코드만이 아니라 재시도 힌트까지 읽을 수 있습니다. 그리고 프리플라이트를 계량하기 위해 CORS 레이어 바깥에 있는 전역 장착 지점은, 자기가 즉시 반환하는429에 allow-origin 헤더를 직접 실어 정체 불명의 네트워크 오류로 남지 않게 합니다.
키별 제한에서 거절된 요청도 전역 예산 토큰 하나와 해당 주소의 클라이언트별 토큰 하나는 이미 소비한 상태입니다. 안쪽 장착 지점이 거절하기 전에, 들어오는 길에 이미 차감되었기 때문입니다.
헤더가 드러내는 것과 드러내지 않는 것¶
x-ratelimit-*는 호출자가 인증을 통해 증명한 식별 정보에 묶인 버킷을 설명하므로, 그 값을 읽어도 이미 본인 것인 정보 외에는 알 수 없습니다. 그 차원이 평가되지 않은 응답에는 할당량 헤더가 실리지 않습니다. 401, 폴백의 404, /version 같은 미인증 200, 화이트리스트 주소나 bypass_keys 보유자에 대한 응답, 그리고 API 경로에 대한 미인증 호출자의 응답이 모두 여기에 해당합니다. 예외 없이 동일하므로, 이 중 어느 것도 신호가 되지 않습니다.
이 균일성이 가장 깨지기 쉬운 지점을 짚어 둡니다. 클라이언트별 장착 지점은 인증보다 먼저 실행되므로, 인증이 뒤이어 거절할 요청도 평가합니다. 만약 이 차원의 할당량을 보고한다면, 겉보기에 똑같은 401 두 개에서 bypass_keys 보유자는 면제 센티널 때문에 헤더를 받지 못하고 다른 토큰은 실제 값을 받게 됩니다. 헤더의 존재 여부만으로 의심되는 우회 키를 요청 한 번에 확인할 수 있다는 뜻입니다. 그래서 클라이언트별 할당량은 보고하지 않으며, 이 동작은 테스트로 고정되어 있습니다.
남아 있는 항목이 하나 있고, 이는 받아들이기로 한 것이므로 분명히 밝혀 둡니다. permissive 인증 모드에서 per_api_key를 설정해 두면, 인식된 키의 응답에는 키별 헤더가 실리고 인식되지 않은 토큰의 응답에는 아무 헤더도 실리지 않으므로, 헤더의 존재 여부만으로 둘이 구분됩니다. 인식되지 않은 호출자에게 헤더를 숨기는 것은 해법이 아닙니다. 그러면 부재가 신호가 될 뿐, 드러나는 정보량은 같습니다.
이를 받아들이기로 한 이유는 permissive 모드가 인증 경계가 아니라 개발 및 신뢰 네트워크용 편의 장치이기 때문입니다. 이 모드에서 라우터는 이미 유효한 키와 무효한 키를 똑같이 처리합니다. 같은 모델, 같은 백엔드, 같은 접근 권한입니다. 키를 열거하는 공격자가 노리는 것은 접근이고 그 접근은 이미 허용되어 있으므로, 키가 인식되는지 알아내도 추가로 얻는 것이 거의 없습니다. 배포 환경에서 키 유효성 자체가 민감하다면 api_keys.mode: blocking을 설정하십시오. 그러면 인식되지 않은 키는 식별 차원이 실행되기 전에 401로 거절되고, 응답이 말해 주는 것은 그 401뿐입니다.
설정¶
기본 설정¶
rate_limiting:
enabled: true
storage: memory # 분산 설정의 경우 "redis"
limits:
per_client:
requests_per_second: 10
burst_capacity: 20
per_backend:
requests_per_second: 100
burst_capacity: 200
global:
requests_per_second: 1000
burst_capacity: 2000
API 키별 속도 제한¶
API 키에는 두 가지 메커니즘이 적용됩니다.
-
rate_limiting.limits안의per_api_key차원: 자체 오버라이드가 없는 모든 인증된 키에 적용되는 기본값입니다. -
api_keys섹션의 개별 키에 거는rate_limit오버라이드 (분당 요청 수): 설정하면 해당 키에서는per_api_key기본값을 대체합니다.rate_limit: 0은 그 키의 키별 제한을 완전히 끕니다.
키별 제한은 인증된 키에만 묶입니다. 이 차원은 인증이 붙여 주는 식별 정보를 읽으므로, 익명 호출자(키가 없거나 알 수 없는 키)는 두 인증 모드 모두에서 키별 제한을 받지 않습니다. 다만 per_client와 global은 인증보다 바깥에 장착되어 있으므로, 요청이 나중에 401로 거절되든 아니든 그대로 적용됩니다.
우회 설정¶
특정 클라이언트는 속도 제한을 완전히 우회할 수 있습니다:
rate_limiting:
# 속도 제한을 우회하는 화이트리스트 IP
whitelist:
- "192.168.1.0/24"
- "10.0.0.1"
# 속도 제한을 우회하는 API 키
bypass_keys:
- "admin-key-123"
- "monitoring-key-456"
클라이언트 식별¶
라우터는 다음 우선순위로 클라이언트를 식별합니다:
- API 키 -
Authorization: Bearer <token>또는x-api-key헤더 (선호) -
다른 IP에서도 정확한 추적 제공
-
X-Forwarded-For 헤더 (프록시/로드 밸런서 시나리오)
- 신뢰할 수 있는 프록시에서 온 요청에만 적용
-
use_rightmost_xff: true(기본값)이면 체인의 가장 오른쪽 IP를 사용하므로 위조가 어려움 -
X-Real-IP 헤더 (대체 프록시 헤더)
-
마찬가지로 신뢰할 수 있는 프록시에서 온 경우에만 적용
-
직접 IP 주소 (프록시 헤더가 없는 경우)
- 요청이 라우터에 직접 도달할 때 사용
신뢰 프록시 설정¶
rate_limiting:
# X-Forwarded-For / X-Real-IP 헤더 설정이 허용되는 프록시
trusted_proxies:
- "10.0.0.0/8"
- "192.168.1.1"
# X-Forwarded-For 체인에서 가장 오른쪽 IP 사용 (기본값: true)
use_rightmost_xff: true
신뢰 목록에 없는 출처가 보낸 포워딩 헤더는 무시되므로, 클라이언트가 X-Forwarded-For를 위조해 클라이언트별 제한을 회피할 수 없습니다.
Unix 소켓 리스너¶
Unix 도메인 소켓 피어에는 IP 주소가 없습니다. 그래서 unix: 리스너에서는 클라이언트별 차원과 IP 화이트리스트가 묶일 대상이 없어 조용히 건너뜁니다. 건너뛸 때마다 rate_limit_skipped_total{dimension="client"} 메트릭과 디버그 로그에 기록됩니다. 전역 제한과 API 키별 제한은 이런 리스너에서도 정상적으로 집행됩니다.
속도 제한 전략¶
토큰 버킷 알고리즘¶
라우터는 장기 속도 제한을 유지하면서 버스트 트래픽을 허용하는 토큰 버킷 알고리즘을 사용합니다:
- 버킷 용량: 최대 토큰 수 (burst_capacity)
- 리필 속도: 초당 추가되는 토큰 (requests_per_second)
- 토큰 비용: 각 요청은 하나의 토큰을 소비
작동 방식¶
- 각 클라이언트는 토큰이 가득 찬 버킷으로 시작
- 각 요청마다 토큰이 소비됨
- 토큰은 일정한 속도로 리필됨
- 버킷이 비면 요청이 거부됨
모델별 제한¶
클라이언트별, 백엔드별, API 키별, 전역 차원에 더해 개별 모델을 대상으로 제한을 걸 수 있습니다. 저렴한 모델은 그대로 두고 고비용 모델만 보호할 때 유용합니다:
rate_limiting:
limits:
per_model:
gpt-4:
requests_per_second: 2
burst_capacity: 5
gpt-3.5-turbo:
requests_per_second: 20
burst_capacity: 50
응답 헤더¶
성공 응답¶
요청이 속도 제한 내에 있고, 호출자 범위 차원(per_api_key 또는 per_client)이 그 요청을 평가했을 때:
이 값은 호출자 본인의 버킷을 설명합니다. 공유 차원(global, per_model, per_backend)만 설정한 경우 허용된 응답에는 X-RateLimit-*가 아예 실리지 않습니다. 공유 버킷의 잔량은 호출자의 할당량이 아니라 전체 부하 총량이기 때문입니다. 헤더 귀속과 거절 우선순위를 참고하십시오.
속도 제한 초과 응답¶
속도 제한이 초과되었을 때:
HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 20
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1640995230
Content-Type: application/json
{
"error": {
"message": "Rate limit exceeded. Try again in 30 seconds.",
"type": "rate_limit_exceeded",
"code": 429,
"details": {
"limit_type": "api_key",
"limit": 20,
"remaining": 0,
"reset_time": 1640995230
}
}
}
Retry-After는 항상 1 이상입니다. 거부된 요청이 Retry-After: 0을 광고하는 일은 없으므로, 방금 요청을 거부한 엔드포인트에 즉시 재시도하라는 안내가 나가지 않습니다. error.details.limit_type은 요청을 차단한 차원(global, client, api_key, backend, model:<이름>)을 알려 줍니다. 두 장착 지점 모두 같은 형태의 스로틀링 헤더와 본문을 내보냅니다. 여러 차원이 동시에 적용될 때 어느 차원의 값이 실리는지는 위의 "헤더 귀속과 거절 우선순위"를 참고하십시오. 브라우저 클라이언트에는 차이가 하나 있습니다. 전역 장착 지점은 CORS 계층 바깥에 있으므로 그 429에는 Access-Control-Allow-Origin이 실리지 않고, 브라우저는 이를 읽을 수 있는 429 대신 불투명한 네트워크 오류로 보고합니다. 페이지가 백오프할 근거를 얻지 못한다는 뜻입니다. 식별 차원의 429는 CORS 계층 안쪽에서 나가므로 이 헤더가 실립니다.
모니터링¶
메트릭¶
속도 제한 동작은 Prometheus 메트릭에서 추적됩니다:
# 수행된 총 속도 제한 검사 수
rate_limit_checks_total 10342
# 거부된 요청 (제한 차원별 레이블)
rate_limit_exceeded_total{limit_type="per_client"} 42
# 현재 토큰 버킷 잔량과 용량
rate_limit_tokens_remaining{bucket_type="per_client",identifier="..."} 15
rate_limit_bucket_capacity{bucket_type="per_client"} 20
# 화이트리스트 및 우회(API 키) 요청
rate_limit_whitelisted_requests_total 123
rate_limit_bypassed_requests_total 45
# 요청에 해당 식별 정보가 없어 건너뛴 차원 (예: Unix 소켓 리스너의
# 클라이언트별 차원, 익명 호출자의 키별 차원). 이 카운터가 꾸준히 늘어난다면
# 설정한 제한이 실제로 묶이지 않고 있다는 신호입니다.
rate_limit_skipped_total{dimension="client"} 7
로깅¶
속도 제한 이벤트는 컨텍스트와 함께 로깅됩니다:
{
"level": "warn",
"msg": "속도 제한 초과",
"client_id": "abc123...",
"endpoint": "/v1/chat/completions",
"limit_type": "burst",
"limit_value": 20,
"window": "5s"
}
우회 메커니즘¶
IP 화이트리스트¶
신뢰할 수 있는 IP 주소나 CIDR 범위를 화이트리스트에 추가:
rate_limiting:
whitelist:
- "192.168.1.0/24" # 내부 네트워크
- "10.0.0.1" # 관리자 서버
- "172.16.0.0/16" # 회사 네트워크
API 키 우회¶
특정 API 키는 모든 속도 제한을 우회할 수 있습니다:
헬스 체크와 메트릭 면제¶
활성 상태 검사 경로 두 개는 항상 면제됩니다. 포화된 라우터도 자기 프로브에 계속 답할 수 있어야 과부하 도중에 재시작당하지 않습니다.
/health/healthz
메트릭 엔드포인트도 면제되지만, 설정된 metrics.path 위치에 대해 metrics.enabled가 참인 동안에만 적용됩니다. 메트릭을 끈 상태에서는 /metrics도 다른 매칭되지 않은 경로처럼 계량됩니다. 이 면제 때문에 리미터는 스크레이프 폭주를 막아 주지 않고, 스크레이프 비용은 메트릭 카디널리티에 비례해 커지므로, metrics.auth를 설정하고 네트워크 수준에서도 엔드포인트를 제한하십시오.
버킷 용량¶
식별별 버킷 맵(per_client, per_api_key, per_backend)은 각각 최대 100,000개 항목을 담으며, 비활성 항목은 600초 후 만료됩니다. 만료 정리 후에도 맵이 가득 차 있으면 해당 요청에서 그 차원을 건너뜁니다. 요청은 허용되고 나머지 차원은 그대로 집행됩니다.
이는 의도한 선택입니다. 거절해도 메모리를 아끼지 못합니다. 거절 경로 역시 버킷을 만들지 않기 때문입니다. 반면 서로 다른 출발 주소 100,000개는 IPv6 /64 하나로도 쉽게 만들 수 있으므로, 닫히는 쪽으로 실패하면 공격자가 한정된 맵을 새 호출자 전원에 대한 10분짜리 장애로 바꿀 수 있습니다. 건너뛴 사실은 rate_limit_skipped_total{dimension}과, 분당 한 줄로 제한된 경고 로그로 확인할 수 있습니다.
스토리지 백엔드¶
메모리 스토리지 (기본)¶
인메모리 스토리지는 빠르지만 인스턴스 간 공유되지 않습니다:
장점:
- 외부 의존성 없음
- 낮은 지연 시간
- 간단한 설정
단점:
- 라우터 인스턴스 간 공유되지 않음
- 재시작 시 손실
- 단일 인스턴스 배포에만 적합
Redis 스토리지 (분산)¶
Redis 스토리지는 여러 라우터 인스턴스 간 분산 속도 제한을 가능하게 합니다:
rate_limiting:
storage: redis
redis:
url: "redis://localhost:6379"
key_prefix: "continuum:ratelimit:" # 속도 제한 키 접두사 (기본값)
ttl: 3600 # 속도 제한 키 TTL (초)
자격 증명은 URL에 포함할 수 있습니다 (redis://:password@host:6379). rate_limiting.redis.url 필드는 ${VAR} 참조를 확장하지 않으므로 검증 전에 완성된 URL을 렌더링하십시오.
장점:
- 모든 라우터 인스턴스 간 공유
- 재시작 시에도 유지
- 정확한 전역 제한
단점:
- Redis 인프라 필요
- 약간 더 높은 지연 시간
- 추가적인 운영 복잡성
핫 리로드 지원¶
속도 제한 설정은 즉시 업데이트를 위한 핫 리로드를 지원합니다:
# 이 설정들은 재시작 없이 즉시 업데이트됩니다
rate_limiting:
enabled: true # ✅ 즉시: 속도 제한 활성화/비활성화
limits:
per_client:
requests_per_second: 10 # ✅ 즉시: 새 제한이 즉시 적용
burst_capacity: 20 # ✅ 즉시: 버스트 설정이 즉시 업데이트
per_backend:
requests_per_second: 100 # ✅ 즉시: 백엔드 제한이 즉시 업데이트
모범 사례¶
1. 보수적으로 시작¶
실제 사용에 따라 더 엄격한 제한으로 시작하고 완화하세요:
2. 모니터링 및 조정¶
메트릭을 사용하여 실제 트래픽 패턴을 이해하세요:
rate_limit_exceeded_total메트릭 추적- 합법적인 트래픽 vs 남용 트래픽 식별
- 데이터에 기반하여 제한 조정
3. 다른 계층 사용¶
키별 오버라이드(분당 요청 수)로 사용자 클래스마다 계층화된 속도 제한을 구현하세요:
api_keys:
api_keys:
- key: "free-tier-key"
rate_limit: 60
- key: "pro-tier-key"
rate_limit: 600
- key: "enterprise-tier-key"
rate_limit: 6000
4. 고비용 모델 보호¶
비용이 많이 드는 모델에는 per_model 제한으로 더 엄격한 제한을 적용하세요:
5. 프로덕션에서는 Redis 사용¶
다중 인스턴스 배포의 경우 Redis 사용:
문제 해결¶
일반적인 문제¶
속도 제한이 적용되지 않음¶
증상: 클라이언트가 설정된 제한을 초과할 수 있음
해결책: 1. 클라이언트가 화이트리스트에 있는지 확인 2. rate_limiting.enabled가 true인지 확인 3. 속도 제한 초기화에 대한 로그 확인 4. API 키 형식이 올바른지 확인
너무 많은 오탐지¶
증상: 합법적인 트래픽이 속도 제한됨
해결책: 1. 버스트 트래픽에 대해 burst_capacity 증가 2. 클라이언트 식별 검토 (여러 클라이언트가 그룹화될 수 있음) 3. API 키별 제한 사용 고려 4. 합법적인 IP를 화이트리스트에 추가
Redis 연결 문제¶
증상: Redis 스토리지에서 속도 제한이 작동하지 않음
해결책: 1. Redis 연결 확인 2. Redis 인증 확인 3. 연결 풀 설정 검토 4. Redis 성능 모니터링
디버그 로깅¶
디버그 로깅을 켜면 속도 제한 판정 과정을 로그에서 볼 수 있습니다:
관련 문서¶
- 속도 제한 아키텍처 - 구현 세부 사항 및 설계 결정
- 설정 가이드 - 전체 설정 참조
- 관리자 API - 런타임 설정 관리
- 메트릭 - 모니터링 및 관측성
- 오류 처리 - 오류 코드 및 재시도 전략