에러 처리

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.codestring기계가 읽을 수 있는 에러 코드 (UPPER_SNAKE_CASE)
error.messagestring사람이 읽을 수 있는 에러 설명
error.detailsobject에러 관련 추가 정보 (선택)
meta.request_idstring요청 추적용 고유 ID
meta.timestampstring에러 발생 시각 (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 메커니즘 구현 (가능한 경우)

다음 단계#