방법: 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 응답이 발생하면 항상 재인증 후 재시도해야 합니다.
  • 오류 분석 누락: 의미 있는 진단 정보를 얻기 위해 오류 응답의 메시지 필드를 항상 분석하고 기록하십시오.