방법: API 오류 처리하기
목표: 프로덕션 API 통합을 위한 견고한 오류 처리 및 재시도 로직 구현
소요 시간: 구현에 30~45분
사용 시점: 네트워크 문제, 인증 만료 및 서비스 중단을 원활하게 처리해야 하는 생산 환경 통합 시스템을 구축할 때.
API 반환 코드
| 코드 | 설명 | 조치 |
| 2억 | 성공 (GET) | 공정 응답 데이터 |
| 201 | 작성 성공 | 리소스가 성공적으로 생성되었습니다 |
| 202 | 업데이트 성공 | 리소스가 성공적으로 업데이트되었습니다 |
| 400 | 잘못된 API 문자열 / 잘못된 요청 | URL 의 형식, 매개변수 및 요청 본문을 확인하세요 |
| 401 | 권한 없음 | 다시 인증하고 다시 시도하세요 |
| 403 | 금지됨 | 사용자 권한 확인 |
| 404 | 찾을 수 없음 | 리소스(프로젝트, 테이블, 기간)가 존재하는지 확인 |
| 500 | 서버 내부 오류 | 지수적 지연 시간을 적용하여 다시 시도해 주십시오. 문제가 지속될 경우 고객 지원팀에 문의해 주십시오 |
오류 응답 이해하기
오류 응답에는 문제를 설명하는 message 필드가 포함된 JSON이 반환됩니다:
{
"message": "Poorly formatted date string: 'SOMEDATE:2010'. Ought to be of the form 'granularity':'year'."
}
흔히 나타나는 오류 메시지:
| 메시지 패턴 | 원인 | 솔루션 |
| 날짜 문자열 형식이 올바르지 않습니다 | 시간 범위 형식이 잘못되었습니다 | 월:연도 형식을 사용하십시오(예: January:2024 ) |
| 사용자 X가 존재하지 않습니다 | 요청된 사용자 이름이 유효하지 않습니다 | 사용자 이름 형식을 확인하고 계정이 존재하는지 확인합니다 |
| 액세스가 거부됨 | 권한 부족 | 계정에 필요한 권한 세트가 있는지 확인하십시오 |
| 테이블을 찾을 수 없습니다 | 대상 테이블이 존재하지 않습니다 | 테이블 이름의 철자를 확인하고 프로젝트를 확인하세요 |
재시도 로직 구현
프로덕션 통합에서는 일시적인 오류 발생 시 지수적 백오프를 적용해야 합니다:
import requests
import time
import logging
class TBMStudioAPIClient:
"""Production-ready TBM Studio API client with retry logic."""
def __init__(self, customer_id, domain, max_retries=3, base_delay=1.0):
self.customer_id = customer_id
self.domain = domain
self.max_retries = max_retries
self.base_delay = base_delay
self.token = None
self.env_id = None
self.logger = logging.getLogger(__name__)
def authenticate(self, public_key, secret_key):
"""Authenticate and store token."""
url = "https://frontdoor.apptio.com/service/apikeylogin"
response = self._make_request(
"POST", url,
json={"keyAccess": public_key, "keySecret": secret_key},
headers={"Content-Type": "application/json"}
)
self.token = response.cookies.get('apptio-opentoken')
# Get environment ID
env_url = f"https://frontdoor.apptio.com/api/environment/{self.domain}/main"
env_response = self._make_request("GET", env_url)
self.env_id = env_response.json()["id"]
def _make_request(self, method, url, **kwargs):
"""Make HTTP request with retry logic."""
last_exception = None
for attempt in range(self.max_retries):
try:
# Add auth headers if authenticated
if self.token and 'headers' not in kwargs:
kwargs['headers'] = {}
if self.token:
kwargs['headers'].update({
"apptio-opentoken": self.token,
"apptio-current-environment": str(self.env_id) if self.env_id else "",
"app-type": "Flagship",
"app-version": "NA"
})
response = requests.request(method, url, **kwargs)
# Check for retryable status codes
if response.status_code == 401:
self.logger.warning("Token expired, re-authentication required")
raise AuthenticationError("Token expired")
if response.status_code >= 500:
response.raise_for_status() # Will be caught and retried
response.raise_for_status()
return response
except requests.exceptions.RequestException as e:
last_exception = e
if attempt < self.max_retries - 1:
delay = self.base_delay * (2 ** attempt) # Exponential backoff
self.logger.warning(
f"Request failed (attempt {attempt + 1}/{self.max_retries}), "
f"retrying in {delay}s: {e}"
)
time.sleep(delay)
raise last_exception
def upload(self, project, table, time_period, file_path, action="overwrite"):
"""Upload data with error handling."""
import urllib.parse
project_enc = urllib.parse.quote(project)
table_enc = urllib.parse.quote(table)
url = (f"https://{self.customer_id}.apptio.com/biit/api/v1/"
f"{self.domain}/{project_enc}/{table_enc}/{time_period}/{action}")
with open(file_path, 'rb') as f:
response = self._make_request("POST", url, files={'myfile': f})
return response.json()
class AuthenticationError(Exception):
"""Raised when authentication fails or token expires."""
pass
# Usage with error handling
def main():
logging.basicConfig(level=logging.INFO)
client = TBMStudioAPIClient("acme", "acme.com")
try:
client.authenticate("public_key", "secret_key")
result = client.upload(
project="Cost Transparency",
table="GL Data",
time_period="January:2024",
file_path="data.csv"
)
print(f"Upload successful: {result}")
except AuthenticationError:
print("Authentication failed. Check API credentials.")
except requests.exceptions.HTTPError as e:
print(f"API error: {e.response.status_code} - {e.response.text}")
except Exception as e:
print(f"Unexpected error: {e}")토큰 갱신 전략
장시간 실행되는 프로세스의 경우, 사전 예방적인 토큰 갱신을 구현하십시오:
import time
from datetime import datetime, timedelta
class TokenManager:
"""Manage API token lifecycle."""
def __init__(self, client, refresh_margin_minutes=5):
self.client = client
self.refresh_margin = timedelta(minutes=refresh_margin_minutes)
self.token_expiry = None
self.token_lifetime = timedelta(hours=1) # Adjust based on actual expiry
def get_valid_token(self, public_key, secret_key):
"""Get a valid token, refreshing if necessary."""
now = datetime.now()
if (self.token_expiry is None or
now >= self.token_expiry - self.refresh_margin):
self.client.authenticate(public_key, secret_key)
self.token_expiry = now + self.token_lifetime
return self.client.token로그 기록 모범 사례
- 모든 API 호출을 기록합니다: 타임스탬프, 엔드포인트, HTTP 메서드 및 응답 상태를 포함합니다
- 응답 시간 기록: 성능 저하 여부 모니터링
- 절대 자격 증명을 로그에 기록하지 마십시오: 로그에서 토큰, API 키 및 비밀번호를 삭제하십시오
- 오류 응답 기록: 문제 해결을 위해 전체 오류 메시지를 캡처합니다
- 구조화된 로깅을 사용하세요: 파싱과 분석을 용이하게 하기 위해 로그를 JSON 형식으로 작성하세요
주의: API 키, 토큰 또는 개인 식별 정보(PII)와 같은 민감한 데이터는 로그에 기록하지 마십시오. 로그에 포함된 식별 가능한 정보에는 자리 표시자나 해싱을 사용하십시오.
흔히 범하는 실수
- 500 오류에 대해서는 재시도하지 않음: 서버 오류는 대개 일시적인 경우가 많습니다. 재시도 시에는 항상 백오프를 적용해야 합니다.
- 요청 제한 무시: 공식적으로 명시되어 있지는 않지만, 요청을 연달아 빠르게 전송하지 마십시오. 배치 작업 사이에 지연 시간을 추가합니다.
- 토큰 만료 처리 누락: 401 응답이 발생하면 항상 재인증 후 재시도해야 합니다.
- 오류 분석 누락: 의미 있는 진단 정보를 얻기 위해 오류 응답의 메시지 필드를 항상 분석하고 기록하십시오.