에러 처리
API 에러 코드와 처리 방법을 안내합니다.
ONDA Platform Engineering 관리
에러 처리
ONDA Channel 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) |
단, POST /oauth/token은 OAuth 2.0 표준 형식({error, error_description})으로 에러를 반환합니다. 인증 가이드를 참고하세요.
HTTP 상태 코드
2xx - 성공
| 코드 | 의미 | 사용 케이스 |
|---|---|---|
200 OK | 요청 성공 | GET, PATCH, PUT 성공 |
201 Created | 리소스 생성됨 | POST /bookings 성공 |
4xx - 클라이언트 에러
| 코드 | 의미 | 주요 원인 |
|---|---|---|
400 Bad Request | 잘못된 요청 | 필수 파라미터 누락, 형식 오류, 토큰 형식 오류 |
401 Unauthorized | 인증 실패 | 토큰/API 키 없음, 만료, 무효 |
402 Payment Required | 잔액 부족 | 가용 잔액 < 예약 입금가 |
403 Forbidden | 권한 없음 | scope 부족, 다른 앱의 리소스 접근 |
404 Not Found | 리소스 없음 | 잘못된 ID, 삭제된 리소스 |
409 Conflict | 충돌 | 이미 취소된 예약, 상태 전이 불가 |
422 Unprocessable Entity | 처리 불가 | book_token 무효/만료 |
429 Too Many Requests | 요청 과다 | Rate limit 초과 |
5xx - 서버 에러
| 코드 | 의미 | 조치 |
|---|---|---|
500 Internal Server Error | 서버 오류 | 재시도 또는 지원팀 문의 |
501 Not Implemented | 미구현 기능 | voucher / tax-invoices 엔드포인트 (추후 제공) |
502 Bad Gateway | 업스트림(Hub) 오류 | 재시도 |
503 Service Unavailable | 서비스 불가 | 잠시 후 재시도 |
공통 에러 코드
인증 관련 (401)
UNAUTHORIZED
{
"error": {
"code": "UNAUTHORIZED",
"message": "Authentication required"
},
"meta": {
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-02-08T10:30:00Z"
}
}
원인: 인증 정보(X-Api-Key 또는 Authorization: Bearer) 누락
해결 방법: 모든 요청에 인증 헤더를 포함하세요.
INVALID_API_KEY / INVALID_TOKEN / TOKEN_EXPIRED
원인: API 키 또는 Access Token이 유효하지 않거나 만료됨
해결 방법:
def api_call_with_token_refresh(url, token_manager, **kwargs):
"""토큰 만료 시 재발급 후 재시도"""
headers = {"Authorization": f"Bearer {token_manager.get_token()}"}
response = requests.get(url, headers=headers, **kwargs)
if response.status_code == 401:
# oat_ 토큰은 30분 후 만료 — client_credentials로 재발급
token_manager.refresh()
headers = {"Authorization": f"Bearer {token_manager.get_token()}"}
response = requests.get(url, headers=headers, **kwargs)
return response
권한 관련 (403)
SCOPE_DENIED
{
"error": {
"code": "SCOPE_DENIED",
"message": "필요한 권한이 없습니다."
},
"meta": {
"request_id": "550e8400-e29b-41d4-a716-446655440002",
"timestamp": "2026-02-08T10:30:00Z"
}
}
원인: 토큰/API 키의 scope가 부족함 (deny-by-default)
해결 방법: 필요한 scope를 포함하여 토큰을 재발급하거나, 대시보드에서 API 키 권한을 조정하세요.
FORBIDDEN
원인: 다른 앱이 소유한 리소스(예약 등)에 접근
해결 방법: 해당 리소스를 생성한 앱의 자격 증명으로 호출하세요.
검증 관련 (400)
INVALID_REQUEST
{
"error": {
"code": "INVALID_REQUEST",
"message": "book_token is required"
},
"meta": {
"request_id": "550e8400-e29b-41d4-a716-446655440004",
"timestamp": "2026-02-08T10:30:00Z"
}
}
원인: 필수 파라미터 누락(book_token, booker_name, token 등) 또는 형식 오류
해결 방법: 에러 메시지에 표기된 파라미터를 보완하여 재요청하세요.
INVALID_TOKEN / TOKEN_EXPIRED (price-check, 400)
원인: GET /search/price-check의 price_check_token이 무효이거나 만료됨
해결 방법: GET /search/availability/{propertyId}를 다시 호출하여 새 price_check_token을 발급받으세요.
리소스 관련 (404)
NOT_FOUND / BOOKING_NOT_FOUND
{
"error": {
"code": "BOOKING_NOT_FOUND",
"message": "Booking not found"
},
"meta": {
"request_id": "550e8400-e29b-41d4-a716-446655440005",
"timestamp": "2026-02-08T10:30:00Z"
}
}
원인: 요청한 리소스가 존재하지 않음
해결 방법: ID를 확인하거나 목록 API로 유효한 ID를 조회하세요.
예약/결제 관련 (402, 409, 422)
INSUFFICIENT_BALANCE (402)
원인: 운영 환경에서 가용 잔액이 예약 입금가보다 적음
해결 방법: 대시보드에서 보증금을 충전한 후 재시도하세요.
BALANCE_SUSPENDED (403)
원인: 가용 잔액이 기준 미달이어서 판매가 비활성화됨
해결 방법: 보증금을 충전하면 판매가 재개됩니다.
BOOKING_TOKEN_INVALID (422)
{
"error": {
"code": "BOOKING_TOKEN_INVALID",
"message": "book_token is invalid or expired"
},
"meta": {
"request_id": "550e8400-e29b-41d4-a716-446655440006",
"timestamp": "2026-02-08T10:30:00Z"
}
}
원인: book_token이 만료되었거나 위·변조됨
해결 방법: GET /search/price-check를 다시 호출하여 새 book_token을 발급받으세요.
BOOKING_NOT_CANCELLABLE (409)
원인: 현재 상태에서 취소할 수 없는 예약
해결 방법: GET /bookings/{id}/cancellation-policy로 취소 가능 여부를 확인하고 고객에게 안내하세요.
BOOKING_ALREADY_CANCELLED (409)
{
"error": {
"code": "BOOKING_ALREADY_CANCELLED",
"message": "이미 취소된 예약입니다."
},
"meta": {
"request_id": "550e8400-e29b-41d4-a716-446655440009",
"timestamp": "2026-02-08T10:30:00Z"
}
}
원인: 이미 취소된 예약에 취소를 다시 요청함
해결 방법: 멱등 처리 — 이미 원하는 상태이므로 성공으로 간주해도 됩니다.
BOOKING_NOT_MODIFIABLE (409) / NO_CHANGES (400)
원인: 수정 불가 상태의 예약(BOOKING_NOT_MODIFIABLE) 또는 변경할 내용 없음(NO_CHANGES)
해결 방법: 예약 상태를 확인하고, 수정 가능한 필드(투숙객 정보)만 보내세요.
업스트림 관련 (502)
HUB_UPSTREAM_ERROR
원인: ONDA Hub 업스트림 응답 오류 (검색/숙소 조회 시)
해결 방법: 지수 백오프로 재시도하세요. 지속되면 request_id와 함께 지원팀에 문의하세요.
가격 변동과 재고 소진은 에러가 아닙니다. GET /search/price-check는 가격이 변했거나 매진된 경우에도 200 OK를 반환하며, 응답의 status 필드가 matched / price_changed / sold_out 중 하나로 표시됩니다. price_changed면 새 가격을 고객에게 확인받은 뒤 함께 발급된 새 book_token으로 예약하고, sold_out이면 다른 객실을 제안하세요.
Rate Limiting (429)
RATE_LIMIT_EXCEEDED
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "요청이 너무 많습니다. 잠시 후 다시 시도해 주세요."
},
"meta": {
"request_id": "550e8400-e29b-41d4-a716-446655440010",
"timestamp": "2026-02-08T10:30:00Z"
}
}
Rate Limit 정책
전체 엔드포인트에 단일 분당 한도가 적용됩니다 (엔드포인트별 차등 없음):
| 환경/등급 | 제한 |
|---|---|
| Sandbox | 60 요청/분 |
| Starter (운영) | 600 요청/분 |
토큰 발급(POST /oauth/token)은 별도로 IP당 60초에 20회로 제한됩니다.
Rate Limit 헤더
응답 헤더에 현재 상태가 포함됩니다:
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 542
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) < 60:
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.tport.dev/channel/v1/properties",
headers=headers
)
)
재시도 가능한 에러
다음 에러는 재시도할 가치가 있습니다:
| 상태 코드 | 재시도 권장 | 전략 |
|---|---|---|
429 Too Many Requests | ✅ 예 | Retry-After 헤더 사용 |
500 Internal Server Error | ✅ 예 | 지수 백오프 |
502 Bad Gateway | ✅ 예 | 지수 백오프 |
503 Service Unavailable | ✅ 예 | 지수 백오프 |
400 Bad Request | ❌ 아니오 | 요청 수정 필요 |
401 Unauthorized | ❌ 아니오 | 토큰 재발급 필요 |
404 Not Found | ❌ 아니오 | 재시도 불필요 |
422 Unprocessable Entity | ❌ 아니오 | 새 book_token 발급 필요 |
POST /bookings 재시도는 주의하세요. 타임아웃으로 응답을 받지 못한 경우 예약이 생성되었을 수 있습니다. 재시도 전에 GET /bookings?affiliate_reference_id={내부 예약번호}로 중복 여부를 확인하세요.
에러 로깅 권장 사항
로깅할 정보
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 사용량: 제한 대비 사용량
에러 처리 체크리스트
- 모든 API 호출에 에러 처리 구현
- 401 에러 시 자동 토큰 재발급
- 429 에러 시 재시도 로직 구현
- 5xx 에러 시 지수 백오프 재시도
- price-check
status필드(price_changed/sold_out) 분기 처리 - 모든 에러를 로그에 기록 (
request_id포함) - 중요 에러는 알림 발송
- 에러율 및 응답 시간 모니터링
다음 단계
- OAuth 2.0 인증 - 인증 에러 해결
- 실시간 검색 - 검색 에러 처리
- 예약 관리 - 예약 에러 처리
- OpenAPI 명세 - 전체 API 명세