에러 처리
API 에러 코드와 처리 방법을 안내합니다.
에러 처리#
ONDA API의 에러 응답 구조와 처리 방법을 안내합니다.
에러 응답 형식#
모든 에러 응답은 일관된 JSON 구조를 따릅니다:
{
"error": {
"code": "ERROR_CODE",
"message": "사람이 읽을 수 있는 에러 메시지",
"details": {
// 추가 컨텍스트 정보 (선택)
}
},
"meta": {
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-02-08T10:30:00Z"
}
}
| 필드 | 타입 | 설명 |
|---|---|---|
error.code | string | 기계가 읽을 수 있는 에러 코드 (UPPER_SNAKE_CASE) |
error.message | string | 사람이 읽을 수 있는 에러 설명 |
error.details | object | 에러 관련 추가 정보 (선택) |
meta.request_id | string | 요청 추적용 고유 ID |
meta.timestamp | string | 에러 발생 시각 (ISO 8601) |
HTTP 상태 코드#
2xx - 성공#
| 코드 | 의미 | 사용 케이스 |
|---|---|---|
200 OK | 요청 성공 | GET, PATCH 성공 |
201 Created | 리소스 생성됨 | POST 성공 |
204 No Content | 성공, 본문 없음 | DELETE 성공 |
4xx - 클라이언트 에러#
| 코드 | 의미 | 주요 원인 |
|---|---|---|
400 Bad Request | 잘못된 요청 | 파라미터 오류, 검증 실패 |
401 Unauthorized | 인증 실패 | 토큰 없음, 만료, 무효 |
403 Forbidden | 권한 없음 | 권한 부족, IP 차단 |
404 Not Found | 리소스 없음 | 잘못된 ID, 삭제된 리소스 |
409 Conflict | 충돌 | 중복 예약, 재고 부족 |
422 Unprocessable Entity | 처리 불가 | 비즈니스 로직 위반 |
429 Too Many Requests | 요청 과다 | Rate limit 초과 |
5xx - 서버 에러#
| 코드 | 의미 | 조치 |
|---|---|---|
500 Internal Server Error | 서버 오류 | 재시도 또는 지원팀 문의 |
502 Bad Gateway | 게이트웨이 오류 | 재시도 |
503 Service Unavailable | 서비스 불가 | 잠시 후 재시도 |
504 Gateway Timeout | 타임아웃 | 재시도 |
공통 에러 코드#
인증 관련 (401)#
INVALID_TOKEN#
{
"error": {
"code": "INVALID_TOKEN",
"message": "Invalid or revoked API key"
},
"meta": {
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-02-08T10:30:00Z"
}
}
원인: API 키가 만료되었거나 유효하지 않음
해결 방법:
def api_call_with_retry(url, headers, **kwargs):
"""인증 실패 시 키 재확인"""
response = requests.get(url, headers=headers, **kwargs)
if response.status_code == 401:
error = response.json()
if error["error"]["code"] == "INVALID_TOKEN":
# API 키 재확인
headers["x-api-key"] = get_valid_api_key()
# 재시도
response = requests.get(url, headers=headers, **kwargs)
return response
INVALID_TOKEN (Missing)#
{
"error": {
"code": "INVALID_TOKEN",
"message": "Missing x-api-key header"
},
"meta": {
"request_id": "550e8400-e29b-41d4-a716-446655440001",
"timestamp": "2026-02-08T10:30:00Z"
}
}
원인: Authorization 헤더가 누락됨
해결 방법: 모든 요청에 Authorization: Bearer {token} 헤더 포함
권한 관련 (403)#
SCOPE_DENIED#
{
"error": {
"code": "SCOPE_DENIED",
"message": "API key does not have the required scope: reservations:write"
},
"meta": {
"request_id": "550e8400-e29b-41d4-a716-446655440002",
"timestamp": "2026-02-08T10:30:00Z"
}
}
원인: 토큰의 scope가 부족함
해결 방법: 필요한 scope를 포함하여 토큰 재발급
IP_BLOCKED#
{
"error": {
"code": "IP_BLOCKED",
"message": "Your IP address is not whitelisted",
"details": {
"ip_address": "203.0.113.45"
}
},
"meta": {
"request_id": "550e8400-e29b-41d4-a716-446655440003",
"timestamp": "2026-02-08T10:30:00Z"
}
}
원인: IP 화이트리스트에 없는 IP에서 요청
해결 방법: 파트너 센터에서 IP 주소를 화이트리스트에 추가
검증 관련 (400, 422)#
VALIDATION_ERROR#
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Check-in date must be in the future"
},
"meta": {
"request_id": "550e8400-e29b-41d4-a716-446655440004",
"timestamp": "2026-02-08T10:30:00Z"
}
}
원인: 요청 파라미터가 검증 규칙을 위반함
해결 방법:
def validate_search_params(params):
"""검색 파라미터 검증"""
errors = []
# 체크인 날짜는 미래여야 함
if datetime.strptime(params["check_in"], "%Y-%m-%d") <= datetime.now():
errors.append({
"field": "check_in",
"message": "Check-in date must be in the future"
})
# 체크아웃은 체크인 이후여야 함
if params["check_out"] <= params["check_in"]:
errors.append({
"field": "check_out",
"message": "Check-out must be after check-in"
})
# 성인 수는 1-10명
if not 1 <= params["adults"] <= 10:
errors.append({
"field": "adults",
"message": "Adults must be between 1 and 10"
})
if errors:
raise ValidationError(errors)
return params
리소스 관련 (404)#
NOT_FOUND#
{
"error": {
"code": "NOT_FOUND",
"message": "The requested resource was not found"
},
"meta": {
"request_id": "550e8400-e29b-41d4-a716-446655440005",
"timestamp": "2026-02-08T10:30:00Z"
}
}
원인: 요청한 리소스가 존재하지 않음
해결 방법: ID를 확인하거나 리소스 목록 API로 유효한 ID 조회
비즈니스 로직 관련 (409, 422)#
NO_AVAILABILITY#
{
"error": {
"code": "NO_AVAILABILITY",
"message": "No rooms available for the requested dates",
"details": {
"property_id": "117417",
"check_in": "2026-02-15",
"check_out": "2026-02-16"
}
},
"meta": {
"request_id": "550e8400-e29b-41d4-a716-446655440006",
"timestamp": "2026-02-08T10:30:00Z"
}
}
원인: 선택한 객실 타입의 재고가 없음
해결 방법: 다른 객실 타입 선택 또는 날짜 변경
PRICE_CHANGED#
{
"error": {
"code": "PRICE_CHANGED",
"message": "The price has changed since your search",
"details": {
"old_price": 150000,
"new_price": 168000,
"difference": 18000,
"currency": "KRW"
}
},
"meta": {
"request_id": "550e8400-e29b-41d4-a716-446655440007",
"timestamp": "2026-02-08T10:30:00Z"
}
}
원인: 검색 시점과 예약 시점 사이에 가격이 변경됨
해결 방법: 새로운 가격을 확인하고 재시도
NOT_CANCELLABLE#
{
"error": {
"code": "NOT_CANCELLABLE",
"message": "This booking cannot be cancelled in its current state"
},
"meta": {
"request_id": "550e8400-e29b-41d4-a716-446655440008",
"timestamp": "2026-02-08T10:30:00Z"
}
}
원인: 환불 불가 요금제 또는 무료 취소 기한 경과
해결 방법: 취소 정책 확인 및 고객 안내
ALREADY_CANCELLED#
{
"error": {
"code": "ALREADY_CANCELLED",
"message": "This booking has already been cancelled"
},
"meta": {
"request_id": "550e8400-e29b-41d4-a716-446655440009",
"timestamp": "2026-02-08T10:30:00Z"
}
}
원인: 동일한 조건의 예약이 이미 존재함
해결 방법: 기존 예약 확인 또는 중복 방지 로직 구현
Rate Limiting (429)#
RATE_LIMIT_EXCEEDED#
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many requests. Please retry after the specified time.",
"details": {
"retry_after_seconds": 45,
"limit": 100
}
},
"meta": {
"request_id": "550e8400-e29b-41d4-a716-446655440010",
"timestamp": "2026-02-08T10:30:00Z"
}
}
원인: 분당 요청 제한(1000건)을 초과함
해결 방법: retry_after 초 후에 재시도
Rate Limit 정책#
| 레벨 | 제한 | 적용 대상 |
|---|---|---|
| 기본 | 1000 요청/분 | 대부분의 엔드포인트 |
| 검색 | 500 요청/분 | 검색 API |
| 예약 | 200 요청/분 | 예약 생성 |
Rate Limit 헤더#
응답 헤더에 현재 상태가 포함됩니다:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 856
X-RateLimit-Reset: 1709370060
구현 예시:
import time
def api_call_with_rate_limit(url, headers, **kwargs):
"""Rate limit을 고려한 API 호출"""
response = requests.get(url, headers=headers, **kwargs)
if response.status_code == 429:
# Retry-After 헤더 확인
retry_after = int(response.headers.get("Retry-After", 60))
print(f"Rate limit exceeded. Retrying after {retry_after}s...")
time.sleep(retry_after)
# 재시도
response = requests.get(url, headers=headers, **kwargs)
# Rate limit 상태 로깅
remaining = response.headers.get("X-RateLimit-Remaining")
if remaining and int(remaining) < 100:
print(f"Warning: Only {remaining} requests remaining")
return response
재시도 전략#
지수 백오프#
import time
import random
def api_call_with_exponential_backoff(
func,
max_retries=5,
base_delay=1,
max_delay=32
):
"""지수 백오프를 사용한 재시도"""
for attempt in range(max_retries):
try:
response = func()
# 성공
if response.status_code < 500:
return response
# 서버 에러 (5xx)
if attempt < max_retries - 1:
# 지수 백오프 계산
delay = min(base_delay * (2 ** attempt), max_delay)
# Jitter 추가 (동시 재시도 방지)
delay += random.uniform(0, delay * 0.1)
print(f"Attempt {attempt + 1} failed. Retrying in {delay:.2f}s...")
time.sleep(delay)
else:
# 최대 재시도 횟수 초과
return response
except requests.exceptions.RequestException as e:
if attempt < max_retries - 1:
delay = min(base_delay * (2 ** attempt), max_delay)
print(f"Request failed: {e}. Retrying in {delay:.2f}s...")
time.sleep(delay)
else:
raise
return None
# 사용 예시
response = api_call_with_exponential_backoff(
lambda: requests.get(
"https://api.onda.me/v1/properties",
headers=headers
)
)
재시도 가능한 에러#
다음 에러는 재시도할 가치가 있습니다:
| 상태 코드 | 재시도 권장 | 전략 |
|---|---|---|
429 Too Many Requests | ✅ 예 | Retry-After 헤더 사용 |
500 Internal Server Error | ✅ 예 | 지수 백오프 |
502 Bad Gateway | ✅ 예 | 지수 백오프 |
503 Service Unavailable | ✅ 예 | 지수 백오프 |
504 Gateway Timeout | ✅ 예 | 지수 백오프 |
400 Bad Request | ❌ 아니오 | 요청 수정 필요 |
401 Unauthorized | ❌ 아니오 | 토큰 갱신 필요 |
404 Not Found | ❌ 아니오 | 재시도 불필요 |
에러 로깅 권장 사항#
로깅할 정보#
import logging
logger = logging.getLogger(__name__)
def log_api_error(response):
"""API 에러 로깅"""
error_data = response.json()
error = error_data.get("error", {})
meta = error_data.get("meta", {})
logger.error(
"API Error",
extra={
"status_code": response.status_code,
"error_code": error.get("code"),
"error_message": error.get("message"),
"request_id": meta.get("request_id"),
"url": response.url,
"method": response.request.method,
"timestamp": meta.get("timestamp"),
"details": error.get("details"),
}
)
모니터링 메트릭#
다음 메트릭을 추적하세요:
- 에러율: 전체 요청 대비 에러 비율
- 에러 타입: 에러 코드별 발생 빈도
- 응답 시간: API 응답 시간 분포
- Rate limit 사용량: 제한 대비 사용량
# Prometheus 예시
from prometheus_client import Counter, Histogram
api_errors = Counter(
"onda_api_errors_total",
"Total API errors",
["error_code", "status_code"]
)
api_latency = Histogram(
"onda_api_latency_seconds",
"API request latency"
)
def track_api_call(response):
"""API 호출 메트릭 추적"""
if response.status_code >= 400:
error_data = response.json()
api_errors.labels(
error_code=error_data.get("error"),
status_code=response.status_code
).inc()
api_latency.observe(response.elapsed.total_seconds())
에러 처리 체크리스트#
- 모든 API 호출에 에러 처리 구현
- 401 에러 시 자동 토큰 갱신
- 429 에러 시 재시도 로직 구현
- 5xx 에러 시 지수 백오프 재시도
- 검증 에러 시 사용자 친화적 메시지 표시
- 모든 에러를 로그에 기록
- request_id를 에러 로그에 포함
- 중요 에러는 알림 발송
- 에러율 및 응답 시간 모니터링
- Fallback 메커니즘 구현 (가능한 경우)
다음 단계#
- OAuth 2.0 인증 - 인증 에러 해결
- 실시간 검색 - 검색 에러 처리
- 예약 관리 - 예약 에러 처리
- API 레퍼런스 - 전체 API 명세