KR_REST - somaz94/python-study GitHub Wiki
REST(Representational State Transfer)λ μΉ μλΉμ€λ₯Ό μν μν€ν
μ² μ€νμΌλ‘, HTTP νλ‘ν μ½μ νΉμ±μ μ΅λν νμ©νμ¬ λ¦¬μμ€ μ€μ¬μ μΈν°νμ΄μ€λ₯Ό μ 곡νλ€.
from flask import Flask, request, jsonify
from flask_restful import Resource, Api
import logging
from typing import Dict, List, Any, Optional
app = Flask(__name__)
api = Api(app)
# κΈ°λ³Έ REST μλν¬μΈνΈ
class UserResource(Resource):
def get(self, user_id: int) -> Dict[str, Any]:
"""
μ¬μ©μ μ 보 μ‘°ν
Args:
user_id: μ¬μ©μ ID
Returns:
dict: μ¬μ©μ μ 보
"""
# GET μμ² μ²λ¦¬ (μ‘°ν)
return {'user_id': user_id, 'name': 'Test User', 'email': '[email protected]'}
def post(self) -> tuple:
"""
μ μ¬μ©μ μμ±
Returns:
tuple: (μλ΅ λ°μ΄ν°, HTTP μν μ½λ)
"""
# POST μμ² μ²λ¦¬ (μμ±)
data = request.get_json()
# λ°μ΄ν°λ² μ΄μ€ μ μ₯ λ‘μ§ κ΅¬ν
return {'status': 'created', 'user': data}, 201
def put(self, user_id: int) -> Dict[str, Any]:
"""
μ¬μ©μ μ 보 μ
λ°μ΄νΈ
Args:
user_id: μ¬μ©μ ID
Returns:
dict: μ
λ°μ΄νΈ κ²°κ³Ό
"""
# PUT μμ² μ²λ¦¬ (μ 체 μ
λ°μ΄νΈ)
data = request.get_json()
# λ°μ΄ν°λ² μ΄μ€ μ
λ°μ΄νΈ λ‘μ§ κ΅¬ν
return {'status': 'updated', 'user_id': user_id}
def patch(self, user_id: int) -> Dict[str, Any]:
"""
μ¬μ©μ μ 보 λΆλΆ μ
λ°μ΄νΈ
Args:
user_id: μ¬μ©μ ID
Returns:
dict: μ
λ°μ΄νΈ κ²°κ³Ό
"""
# PATCH μμ² μ²λ¦¬ (λΆλΆ μ
λ°μ΄νΈ)
data = request.get_json()
# λ°μ΄ν°λ² μ΄μ€ λΆλΆ μ
λ°μ΄νΈ λ‘μ§ κ΅¬ν
return {'status': 'partially updated', 'user_id': user_id}
def delete(self, user_id: int) -> tuple:
"""
μ¬μ©μ μμ
Args:
user_id: μ¬μ©μ ID
Returns:
tuple: (μλ΅ λ°μ΄ν°, HTTP μν μ½λ)
"""
# DELETE μμ² μ²λ¦¬ (μμ )
# λ°μ΄ν°λ² μ΄μ€ μμ λ‘μ§ κ΅¬ν
return '', 204
# 리μμ€ λΌμ°ν
μ€μ
api.add_resource(UserResource, '/users', '/users/<int:user_id>')
β
νΉμ§:
- HTTP λ©μλ ꡬν (GET, POST, PUT, PATCH, DELETE)
- 리μμ€ κΈ°λ° μ€κ³λ‘ μ§κ΄μ μΈ API ꡬ쑰ν
- REST μμΉμ λ§λ μν μ½λ νμ©
- URIλ₯Ό ν΅ν 리μμ€ μλ³
- νμ νν μ ν΅ν μ½λ κ°λ μ± ν₯μ
- λ©μλ λ¬Έμνλ‘ API μ΄ν΄λ μ¦κ°
- λͺ νν μμ²/μλ΅ κ΅¬μ‘°
- μνλ₯Ό μ μ₯νμ§ μλ 무μν(Stateless) ν΅μ
API μλ²μ ν΅μ νκΈ° μν ν΄λΌμ΄μΈνΈλ requests λΌμ΄λΈλ¬λ¦¬λ₯Ό κΈ°λ°μΌλ‘ ꡬμΆν μ μμΌλ©°, μ¬μ¬μ© κ°λ₯ν μΆμν κ³μΈ΅μ μ 곡νλ€.
import requests
import logging
import json
import time
from typing import Dict, List, Any, Optional, Union
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
class RESTClient:
"""REST APIμ ν΅μ νκΈ° μν ν΄λΌμ΄μΈνΈ ν΄λμ€"""
def __init__(
self,
base_url: str,
api_key: Optional[str] = None,
timeout: int = 30,
max_retries: int = 3,
retry_backoff_factor: float = 0.5,
retry_on_status: List[int] = None
):
"""
REST ν΄λΌμ΄μΈνΈ μ΄κΈ°ν
Args:
base_url: API κΈ°λ³Έ URL
api_key: API ν€ (μλ κ²½μ°)
timeout: μμ² νμμμ (μ΄)
max_retries: μ΅λ μ¬μλ νμ
retry_backoff_factor: μ¬μλ κ°κ²© κ³μ
retry_on_status: μ¬μλν HTTP μν μ½λ λͺ©λ‘
"""
self.logger = logging.getLogger(__name__)
self.base_url = base_url.rstrip('/')
self.timeout = timeout
# μΈμ
μ΄κΈ°ν
self.session = requests.Session()
# μΈμ¦ ν€λ μ€μ
if api_key:
self.session.headers.update({
'Authorization': f'Bearer {api_key}',
'Content-Type': 'application/json'
})
else:
self.session.headers.update({
'Content-Type': 'application/json'
})
# μ¬μλ μ€μ
if retry_on_status is None:
retry_on_status = [429, 500, 502, 503, 504]
retry_strategy = Retry(
total=max_retries,
backoff_factor=retry_backoff_factor,
status_forcelist=retry_on_status,
allowed_methods=["GET", "POST", "PUT", "DELETE", "PATCH"]
)
adapter = HTTPAdapter(max_retries=retry_strategy)
self.session.mount("http://", adapter)
self.session.mount("https://", adapter)
def __del__(self):
"""μλ©Έμ: μΈμ
μ 리"""
if hasattr(self, 'session'):
self.session.close()
def _build_url(self, endpoint: str) -> str:
"""
μ 체 URL ꡬμ±
Args:
endpoint: API μλν¬μΈνΈ
Returns:
str: μμ ν URL
"""
return f"{self.base_url}/{endpoint.lstrip('/')}"
def _handle_response(self, response: requests.Response) -> Any:
"""
API μλ΅ μ²λ¦¬
Args:
response: μμ² μλ΅ κ°μ²΄
Returns:
Any: μλ΅ λ°μ΄ν°
Raises:
requests.HTTPError: HTTP μ€λ₯ λ°μ μ
"""
try:
response.raise_for_status()
if not response.content:
return None
return response.json()
except json.JSONDecodeError:
self.logger.warning("μλ΅μ΄ JSON νμμ΄ μλλλ€")
return response.text
except requests.HTTPError as e:
self.logger.error(f"HTTP μ€λ₯: {e}")
# μ€λ₯ μλ΅ λ΄μ© μΆμΆ μλ
error_detail = None
try:
error_detail = response.json()
except:
error_detail = response.text
self.logger.error(f"μ€λ₯ μΈλΆ μ 보: {error_detail}")
raise
def get(
self,
endpoint: str,
params: Optional[Dict[str, Any]] = None,
headers: Optional[Dict[str, str]] = None
) -> Any:
"""
GET μμ² μν
Args:
endpoint: API μλν¬μΈνΈ
params: 쿼리 νλΌλ―Έν°
headers: μΆκ° ν€λ
Returns:
Any: μλ΅ λ°μ΄ν°
"""
url = self._build_url(endpoint)
self.logger.debug(f"GET μμ²: {url}")
start_time = time.time()
response = self.session.get(
url,
params=params,
headers=headers,
timeout=self.timeout
)
elapsed = time.time() - start_time
self.logger.debug(f"μλ΅ μκ°: {elapsed:.2f}μ΄")
return self._handle_response(response)
def post(
self,
endpoint: str,
data: Any,
params: Optional[Dict[str, Any]] = None,
headers: Optional[Dict[str, str]] = None
) -> Any:
"""
POST μμ² μν
Args:
endpoint: API μλν¬μΈνΈ
data: μμ² λ°μ΄ν°
params: 쿼리 νλΌλ―Έν°
headers: μΆκ° ν€λ
Returns:
Any: μλ΅ λ°μ΄ν°
"""
url = self._build_url(endpoint)
self.logger.debug(f"POST μμ²: {url}")
start_time = time.time()
response = self.session.post(
url,
json=data,
params=params,
headers=headers,
timeout=self.timeout
)
elapsed = time.time() - start_time
self.logger.debug(f"μλ΅ μκ°: {elapsed:.2f}μ΄")
return self._handle_response(response)
def put(
self,
endpoint: str,
data: Any,
params: Optional[Dict[str, Any]] = None,
headers: Optional[Dict[str, str]] = None
) -> Any:
"""
PUT μμ² μν
Args:
endpoint: API μλν¬μΈνΈ
data: μμ² λ°μ΄ν°
params: 쿼리 νλΌλ―Έν°
headers: μΆκ° ν€λ
Returns:
Any: μλ΅ λ°μ΄ν°
"""
url = self._build_url(endpoint)
self.logger.debug(f"PUT μμ²: {url}")
start_time = time.time()
response = self.session.put(
url,
json=data,
params=params,
headers=headers,
timeout=self.timeout
)
elapsed = time.time() - start_time
self.logger.debug(f"μλ΅ μκ°: {elapsed:.2f}μ΄")
return self._handle_response(response)
def patch(
self,
endpoint: str,
data: Any,
params: Optional[Dict[str, Any]] = None,
headers: Optional[Dict[str, str]] = None
) -> Any:
"""
PATCH μμ² μν
Args:
endpoint: API μλν¬μΈνΈ
data: μμ² λ°μ΄ν°
params: 쿼리 νλΌλ―Έν°
headers: μΆκ° ν€λ
Returns:
Any: μλ΅ λ°μ΄ν°
"""
url = self._build_url(endpoint)
self.logger.debug(f"PATCH μμ²: {url}")
start_time = time.time()
response = self.session.patch(
url,
json=data,
params=params,
headers=headers,
timeout=self.timeout
)
elapsed = time.time() - start_time
self.logger.debug(f"μλ΅ μκ°: {elapsed:.2f}μ΄")
return self._handle_response(response)
def delete(
self,
endpoint: str,
params: Optional[Dict[str, Any]] = None,
headers: Optional[Dict[str, str]] = None
) -> Any:
"""
DELETE μμ² μν
Args:
endpoint: API μλν¬μΈνΈ
params: 쿼리 νλΌλ―Έν°
headers: μΆκ° ν€λ
Returns:
Any: μλ΅ λ°μ΄ν°
"""
url = self._build_url(endpoint)
self.logger.debug(f"DELETE μμ²: {url}")
start_time = time.time()
response = self.session.delete(
url,
params=params,
headers=headers,
timeout=self.timeout
)
elapsed = time.time() - start_time
self.logger.debug(f"μλ΅ μκ°: {elapsed:.2f}μ΄")
return self._handle_response(response)
β
νΉμ§:
- μΈμ κ΄λ¦¬λ‘ μ°κ²° μ¬μ¬μ© μ΅μ ν
- μΈμ¦ ν€λ μλ μ€μ
- μ¬μλ λ©μ»€λμ¦μΌλ‘ μΌμμ μ€λ₯ μ²λ¦¬
- νμμμ μ€μ μΌλ‘ μλ΅ μ§μ° μ ν
- HTTP λ©μλ μμ μ§μ (GET, POST, PUT, PATCH, DELETE)
- μλ¬ μ²λ¦¬ λ° λ‘κΉ ν΅ν©
- μλ΅ μκ° μΈ‘μ λ° λλ²κΉ
- JSON λ³ν μλν
- νμ νν μ ν΅ν μ½λ κ°λ μ± ν₯μ
- 리μμ€ μ 리λ₯Ό μν μλ©Έμ ꡬν
REST APIμμ μΈμ¦κ³Ό 보μμ μ€μν κ³ λ €μ¬νμΌλ‘, λ€μν λ°©μμ μΈμ¦ λ©μ»€λμ¦κ³Ό 보μ κΈ°λ²μ ν΅ν΄ API 리μμ€λ₯Ό 보νΈν μ μλ€.
from flask import Flask, request, jsonify
from flask_restful import Resource, Api
from functools import wraps
import jwt
import datetime
import uuid
import logging
import hashlib
from typing import Dict, Any, Callable, Optional
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key' # μ€μ νκ²½μμλ νκ²½ λ³μ λ±μΌλ‘ κ΄λ¦¬
api = Api(app)
# λ‘κΉ
μ€μ
logger = logging.getLogger(__name__)
# JWT ν ν° μμ±
def generate_token(user_id: int, expiry_minutes: int = 60) -> str:
"""
JWT ν ν° μμ± ν¨μ
Args:
user_id: μ¬μ©μ ID
expiry_minutes: ν ν° λ§λ£ μκ°(λΆ)
Returns:
str: μμ±λ JWT ν ν°
"""
payload = {
'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=expiry_minutes),
'iat': datetime.datetime.utcnow(),
'sub': user_id,
'jti': str(uuid.uuid4())
}
token = jwt.encode(
payload,
app.config['SECRET_KEY'],
algorithm='HS256'
)
return token
# μΈμ¦ λ°μ½λ μ΄ν°
def require_auth(f: Callable) -> Callable:
"""
μΈμ¦μ΄ νμν API μλν¬μΈνΈμ μ¬μ©νλ λ°μ½λ μ΄ν°
Args:
f: μλ³Έ ν¨μ
Returns:
Callable: λνλ ν¨μ
"""
@wraps(f)
def decorated(*args: Any, **kwargs: Any) -> Any:
# μΈμ¦ ν€λ νμΈ
auth_header = request.headers.get('Authorization')
if not auth_header:
logger.warning("μΈμ¦ ν ν° μμ")
return {'message': 'μΈμ¦ ν ν°μ΄ νμν©λλ€', 'error': 'unauthorized'}, 401
# Bearer ν ν° νμ νμΈ
parts = auth_header.split()
if parts[0].lower() != 'bearer' or len(parts) != 2:
logger.warning("μλͺ»λ μΈμ¦ ν€λ νμ")
return {'message': 'μλͺ»λ μΈμ¦ ν€λ νμ', 'error': 'invalid_header'}, 401
token = parts[1]
try:
# ν ν° κ²μ¦
payload = jwt.decode(
token,
app.config['SECRET_KEY'],
algorithms=['HS256']
)
# μ¬μ©μ μ 보 μ€μ
request.user_id = payload['sub']
return f(*args, **kwargs)
except jwt.ExpiredSignatureError:
logger.warning(f"λ§λ£λ ν ν°: {token[:10]}...")
return {'message': 'ν ν°μ΄ λ§λ£λμμ΅λλ€', 'error': 'token_expired'}, 401
except jwt.InvalidTokenError:
logger.warning(f"μ ν¨νμ§ μμ ν ν°: {token[:10]}...")
return {'message': 'μ ν¨νμ§ μμ ν ν°μ
λλ€', 'error': 'invalid_token'}, 401
except Exception as e:
logger.error(f"μΈμ¦ μ²λ¦¬ μ€ μ€λ₯: {str(e)}")
return {'message': 'μλ² μΈμ¦ μ€λ₯', 'error': 'auth_error'}, 500
return decorated
# API μλ μ ν λ°μ½λ μ΄ν°
def rate_limit(limit: int = 100, period: int = 3600) -> Callable:
"""
API μμ² μλ μ ν λ°μ½λ μ΄ν°
Args:
limit: νμ© μμ² μ
period: μκ° κΈ°κ°(μ΄)
Returns:
Callable: λ°μ½λ μ΄ν° ν¨μ
"""
def decorator(f: Callable) -> Callable:
# μ€μ ꡬνμμλ Redis λ±μ λΆμ° μΊμ μ¬μ© κΆμ₯
request_counts = {}
@wraps(f)
def decorated(*args: Any, **kwargs: Any) -> Any:
# ν΄λΌμ΄μΈνΈ μλ³
client_ip = request.remote_addr
client_id = request.headers.get('X-API-Key', client_ip)
current_time = int(datetime.datetime.utcnow().timestamp())
# μμ² μ κ΄λ¦¬
if client_id not in request_counts:
request_counts[client_id] = {'count': 1, 'reset_time': current_time + period}
else:
if current_time > request_counts[client_id]['reset_time']:
# κΈ°κ° μ΄κΈ°ν
request_counts[client_id] = {'count': 1, 'reset_time': current_time + period}
else:
# μμ² μ μ¦κ°
request_counts[client_id]['count'] += 1
# νλ νμΈ
if request_counts[client_id]['count'] > limit:
reset_time = request_counts[client_id]['reset_time']
reset_in = reset_time - current_time
logger.warning(f"μλ μ ν μ΄κ³Ό: {client_id}")
# μλ μ ν μλ΅ ν€λ
response = jsonify({
'message': 'μμ² νλ μ΄κ³Ό',
'error': 'rate_limit_exceeded'
})
response.status_code = 429
response.headers['X-RateLimit-Limit'] = str(limit)
response.headers['X-RateLimit-Remaining'] = '0'
response.headers['X-RateLimit-Reset'] = str(reset_time)
response.headers['Retry-After'] = str(reset_in)
return response
# μμ² μ²λ¦¬
return f(*args, **kwargs)
return decorated
return decorator
# 보νΈλ 리μμ€ μμ
class SecureResource(Resource):
@require_auth
@rate_limit(limit=5, period=60) # 1λΆλΉ 5κ° μμ²μΌλ‘ μ ν
def get(self) -> Dict[str, Any]:
"""보νΈλ 리μμ€ μ‘°ν"""
return {
'message': '보νΈλ 리μμ€',
'user_id': request.user_id,
'timestamp': datetime.datetime.utcnow().isoformat()
}
# μΈμ¦ μλν¬μΈνΈ
class AuthResource(Resource):
def post(self) -> Dict[str, Any]:
"""μ¬μ©μ μΈμ¦ λ° ν ν° λ°κΈ"""
data = request.get_json()
# κ°λ¨ν μΈμ¦ μμ (μ€μ νκ²½μμλ DB μΈμ¦ μ¬μ©)
username = data.get('username')
password = data.get('password')
# λΉλ°λ²νΈ ν΄μ± μ²λ¦¬ μμ
password_hash = hashlib.sha256(password.encode()).hexdigest()
# μ¬μ©μ κ²μ¦ (μμ)
if username == 'admin' and password_hash == hashlib.sha256('password'.encode()).hexdigest():
# ν ν° μμ±
token = generate_token(user_id=1)
return {
'access_token': token,
'token_type': 'Bearer',
'expires_in': 3600
}
else:
return {'message': 'μΈμ¦ μ€ν¨', 'error': 'invalid_credentials'}, 401
# μλν¬μΈνΈ λ±λ‘
api.add_resource(SecureResource, '/secure')
api.add_resource(AuthResource, '/auth')
β
νΉμ§:
- JWT κΈ°λ° μΈμ¦ μμ€ν ꡬν
- ν ν° μμ± λ° κ²μ¦ λ©μ»€λμ¦
- λ°μ½λ μ΄ν° ν¨ν΄μ νμ©ν μΈμ¦ λ‘μ§ λΆλ¦¬
- μλ μ ν(Rate Limiting) ꡬν
- ν ν° λ§λ£ λ° κ°±μ κ΄λ¦¬
- λ‘κΉ μ ν΅ν 보μ μ΄λ²€νΈ μΆμ
- μμΈν μ€λ₯ λ©μμ§ λ° μ½λ
- HTTP ν€λλ₯Ό νμ©ν μΈμ¦ μ 보 μ λ¬
- 보μ ν€λ μ€μ
- λΉλ°λ²νΈ ν΄μ± μ²λ¦¬
API λ²μ κ΄λ¦¬λ API λ³κ²½μ¬νμ λμ
νλ©΄μ κΈ°μ‘΄ ν΄λΌμ΄μΈνΈμμ νΈνμ±μ μ μ§νκΈ° μν νμμ μΈ μ λ΅μ΄λ€. μ¬λ¬ λ²μ κ΄λ¦¬ λ°©μμ μ μ ν μ‘°ν©νμ¬ μ¬μ©ν μ μλ€.
from flask import Flask, request, jsonify, Blueprint
from flask_restful import Resource, Api, reqparse
import logging
from typing import Dict, Any, Union, Tuple
app = Flask(__name__)
api = Api(app)
logger = logging.getLogger(__name__)
# 1. URL κ²½λ‘ κΈ°λ° λ²μ κ΄λ¦¬
class APIv1(Resource):
"""API λ²μ 1 (URL κΈ°λ°)"""
def get(self) -> Dict[str, Any]:
"""V1 λ°μ΄ν° μ‘°ν"""
return {
'version': '1.0',
'data': 'This is version 1 data',
'features': ['basic_feature']
}
class APIv2(Resource):
"""API λ²μ 2 (URL κΈ°λ°)"""
def get(self) -> Dict[str, Any]:
"""V2 λ°μ΄ν° μ‘°ν (ν₯μλ κΈ°λ₯)"""
return {
'version': '2.0',
'data': 'This is version 2 data with enhancements',
'features': ['basic_feature', 'enhanced_feature']
}
# URLμ λ²μ μ 보 ν¬ν¨
api.add_resource(APIv1, '/v1/resource')
api.add_resource(APIv2, '/v2/resource')
# 2. ν€λ κΈ°λ° λ²μ κ΄λ¦¬
class VersionedHeaderResource(Resource):
"""ν€λ κΈ°λ° λ²μ κ΄λ¦¬"""
def get(self) -> Union[Dict[str, Any], Tuple[Dict[str, Any], int]]:
"""λ²μ λ³ μλ΅ λ°ν"""
# API λ²μ ν€λ νμΈ
version = request.headers.get('API-Version', '1.0')
logger.info(f"μμ²λ API λ²μ : {version}")
if version == '1.0':
return {
'version': '1.0',
'data': 'This is version 1 data via header',
'features': ['basic_feature']
}
elif version == '2.0':
return {
'version': '2.0',
'data': 'This is version 2 data via header with enhancements',
'features': ['basic_feature', 'enhanced_feature']
}
else:
# μ§μνμ§ μλ λ²μ
return {
'error': 'unsupported_version',
'message': f'λ²μ {version}μ μ§μλμ§ μμ΅λλ€. μ§μ λ²μ : 1.0, 2.0'
}, 400
# ν€λ κΈ°λ° λ²μ κ΄λ¦¬μ© μλν¬μΈνΈ
api.add_resource(VersionedHeaderResource, '/resource')
# 3. 쿼리 νλΌλ―Έν° κΈ°λ° λ²μ κ΄λ¦¬
class VersionedQueryResource(Resource):
"""쿼리 νλΌλ―Έν° κΈ°λ° λ²μ κ΄λ¦¬"""
def get(self) -> Union[Dict[str, Any], Tuple[Dict[str, Any], int]]:
"""λ²μ λ³ μλ΅ λ°ν"""
# 쿼리 νλΌλ―Έν°μμ λ²μ νμΈ
parser = reqparse.RequestParser()
parser.add_argument('version', type=str, default='1.0')
args = parser.parse_args()
version = args['version']
logger.info(f"쿼리 νλΌλ―Έν° API λ²μ : {version}")
if version == '1.0':
return {
'version': '1.0',
'data': 'This is version 1 data via query parameter',
'features': ['basic_feature']
}
elif version == '2.0':
return {
'version': '2.0',
'data': 'This is version 2 data via query parameter with enhancements',
'features': ['basic_feature', 'enhanced_feature']
}
else:
# μ§μνμ§ μλ λ²μ
return {
'error': 'unsupported_version',
'message': f'λ²μ {version}μ μ§μλμ§ μμ΅λλ€. μ§μ λ²μ : 1.0, 2.0'
}, 400
# 쿼리 νλΌλ―Έν° κΈ°λ° λ²μ κ΄λ¦¬μ© μλν¬μΈνΈ
api.add_resource(VersionedQueryResource, '/query-resource')
# 4. μ½ν
μΈ νμ κΈ°λ° λ²μ κ΄λ¦¬
class ContentNegotiationResource(Resource):
"""μ½ν
μΈ νμ(Accept ν€λ) κΈ°λ° λ²μ κ΄λ¦¬"""
def get(self) -> Union[Dict[str, Any], Tuple[Dict[str, Any], int]]:
"""λ²μ λ³ μλ΅ λ°ν"""
# Accept ν€λ νμΈ
accept_header = request.headers.get('Accept', 'application/json')
logger.info(f"Accept ν€λ: {accept_header}")
if accept_header == 'application/vnd.api.v1+json':
return {
'version': '1.0',
'data': 'This is version 1 data via content negotiation',
'features': ['basic_feature']
}
elif accept_header == 'application/vnd.api.v2+json':
return {
'version': '2.0',
'data': 'This is version 2 data via content negotiation with enhancements',
'features': ['basic_feature', 'enhanced_feature']
}
else:
# κΈ°λ³Έ μλ΅μ μ΅μ λ²μ μΌλ‘
return {
'version': '2.0',
'data': 'This is default version data',
'features': ['basic_feature', 'enhanced_feature']
}
# μ½ν
μΈ νμ κΈ°λ° λ²μ κ΄λ¦¬μ© μλν¬μΈνΈ
api.add_resource(ContentNegotiationResource, '/content-resource')
# 5. λΈλ£¨νλ¦°νΈλ₯Ό μ¬μ©ν λ²μ κ΄λ¦¬ (λͺ¨λν)
api_v1 = Blueprint('api_v1', __name__, url_prefix='/api/v1')
api_v2 = Blueprint('api_v2', __name__, url_prefix='/api/v2')
# λΈλ£¨νλ¦°νΈλ³ API μΈμ€ν΄μ€ μμ±
api_v1_instance = Api(api_v1)
api_v2_instance = Api(api_v2)
# V1 리μμ€ μ μ
class UserResourceV1(Resource):
def get(self, user_id: int) -> Dict[str, Any]:
"""V1 μ¬μ©μ μ 보 μ‘°ν"""
return {
'id': user_id,
'name': 'User Name',
'version': '1.0'
}
# V2 리μμ€ μ μ (μ νλ μΆκ°)
class UserResourceV2(Resource):
def get(self, user_id: int) -> Dict[str, Any]:
"""V2 μ¬μ©μ μ 보 μ‘°ν (μΆκ° νλ)"""
return {
'id': user_id,
'name': 'User Name',
'email': '[email protected]', # μ νλ
'profile_url': f'/api/v2/users/{user_id}/profile', # μ νλ
'version': '2.0'
}
# λΈλ£¨νλ¦°νΈμ 리μμ€ λ±λ‘
api_v1_instance.add_resource(UserResourceV1, '/users/<int:user_id>')
api_v2_instance.add_resource(UserResourceV2, '/users/<int:user_id>')
# μ±μ λΈλ£¨νλ¦°νΈ λ±λ‘
app.register_blueprint(api_v1)
app.register_blueprint(api_v2)
# λ²μ κ΄λ¦¬ μ μ±
λ° μ€λͺ
νμ΄μ§
@app.route('/api/versions')
def api_versions() -> Dict[str, Any]:
"""API λ²μ μ 보 λ° μ€λͺ
μ 곡"""
return {
'versions': [
{
'version': '1.0',
'status': 'stable',
'released': '2022-01-01',
'sunset_date': '2023-12-31',
'documentation': '/docs/v1'
},
{
'version': '2.0',
'status': 'current',
'released': '2023-01-01',
'sunset_date': None,
'documentation': '/docs/v2'
}
],
'versioning_methods': [
'URL κ²½λ‘ (/v1/resource, /v2/resource)',
'API-Version ν€λ',
'쿼리 νλΌλ―Έν° (?version=1.0)',
'Accept ν€λ (application/vnd.api.v1+json)'
],
'deprecation_policy': 'API λ²μ μ μΆμμΌλ‘λΆν° μ΅μ 1λ
κ° μ§μλ©λλ€.'
}
β
νΉμ§:
- λ€μν λ²μ κ΄λ¦¬ μ λ΅ κ΅¬ν
- URL κ²½λ‘ κΈ°λ° λ²μ κ΄λ¦¬ (/v1/resource)
- HTTP ν€λ κΈ°λ° λ²μ κ΄λ¦¬ (API-Version)
- 쿼리 νλΌλ―Έν° κΈ°λ° λ²μ κ΄λ¦¬ (?version=1.0)
- μ½ν μΈ νμ κΈ°λ° λ²μ κ΄λ¦¬ (Accept ν€λ)
- λΈλ£¨νλ¦°νΈλ₯Ό νμ©ν λͺ¨λμ λ²μ κ΄λ¦¬
- API λ²μ μ 보 λ° μ§μ μ μ± μ 곡
- λ²μ λ³ κΈ°λ₯ μ°¨μ΄ λͺ μ
- λ²μ μ§μ μ’ λ£ μΌμ κ΄λ¦¬
- νμ νν μ ν΅ν μ½λ κ°λ μ± ν₯μ
- λ‘κΉ μ ν΅ν λ²μ μμ² μΆμ
- μ§μνμ§ μλ λ²μ μ λν μ€λ₯ μ²λ¦¬
- κΈ°λ³Έ λ²μ μ€μ μΌλ‘ νμ νΈνμ± μ μ§
REST APIμ μ±κ³΅μ μΈ μ¬μ© λ° λμ
μ μν΄μλ λͺ
ννκ³ μ΄ν΄νκΈ° μ¬μ΄ λ¬Έμκ° νμμ μ΄λ€. μλνλ λ¬Έμ μμ± λꡬλ₯Ό νμ©νλ©΄ μ½λμ λ¬Έμμ μΌκ΄μ±μ μ μ§ν μ μλ€.
from flask import Flask, request, jsonify
from flask_restful import Api, Resource
from apispec import APISpec
from apispec.ext.marshmallow import MarshmallowPlugin
from apispec_webframeworks.flask import FlaskPlugin
from marshmallow import Schema, fields
import logging
from typing import Dict, List, Any, Optional
app = Flask(__name__)
api = Api(app)
# APISpec μ€μ
spec = APISpec(
title="Sample REST API",
version="1.0.0",
openapi_version="3.0.2",
plugins=[FlaskPlugin(), MarshmallowPlugin()],
info={
"description": "μν REST API λ¬Έμ",
"contact": {"email": "[email protected]"}
}
)
# μ€ν€λ§ μ μ
class UserSchema(Schema):
"""μ¬μ©μ λ°μ΄ν° μ€ν€λ§"""
id = fields.Int(required=True, description="μ¬μ©μ ID")
name = fields.Str(required=True, description="μ΄λ¦")
email = fields.Email(required=True, description="μ΄λ©μΌ μ£Όμ")
created_at = fields.DateTime(description="μμ± μκ°")
class ErrorSchema(Schema):
"""μ€λ₯ μλ΅ μ€ν€λ§"""
error = fields.Str(required=True, description="μ€λ₯ μ½λ")
message = fields.Str(required=True, description="μ€λ₯ λ©μμ§")
# API 리μμ€ μ μ
class UserResource(Resource):
def get(self, user_id):
"""
μ¬μ©μ μ 보 μ‘°ν
---
parameters:
- in: path
name: user_id
schema:
type: integer
required: true
description: μ¬μ©μ ID
responses:
200:
description: μ±κ³΅
content:
application/json:
schema: UserSchema
404:
description: μ¬μ©μλ₯Ό μ°Ύμ μ μμ
content:
application/json:
schema: ErrorSchema
"""
return {
"id": user_id,
"name": "Example User",
"email": "[email protected]",
"created_at": "2023-01-01T00:00:00Z"
}
def put(self, user_id):
"""
μ¬μ©μ μ 보 μ
λ°μ΄νΈ
---
parameters:
- in: path
name: user_id
schema:
type: integer
required: true
description: μ¬μ©μ ID
requestBody:
content:
application/json:
schema: UserSchema
responses:
200:
description: μ±κ³΅μ μΌλ‘ μ
λ°μ΄νΈλ¨
content:
application/json:
schema: UserSchema
400:
description: μλͺ»λ μμ²
content:
application/json:
schema: ErrorSchema
404:
description: μ¬μ©μλ₯Ό μ°Ύμ μ μμ
content:
application/json:
schema: ErrorSchema
"""
data = request.get_json()
return {
"id": user_id,
"name": data.get("name", "Updated User"),
"email": data.get("email", "[email protected]"),
"created_at": "2023-01-01T00:00:00Z"
}
# 리μμ€ λ±λ‘
api.add_resource(UserResource, '/users/<int:user_id>')
# μ€ν€λ§ λ±λ‘
spec.components.schema("User", schema=UserSchema)
spec.components.schema("Error", schema=ErrorSchema)
# κ²½λ‘ λ±λ‘
with app.test_request_context():
spec.path(view=UserResource, path="/users/{user_id}", operations={"get": {}, "put": {}})
# Swagger UI λ¬Έμ μ 곡 μλν¬μΈνΈ
@app.route('/swagger.json')
def create_swagger_spec():
"""OpenAPI μ€ν μμ±"""
return jsonify(spec.to_dict())
# Swagger UI νμ΄μ§ μ 곡
@app.route('/docs')
def swagger_ui():
"""Swagger UI HTML νμ΄μ§ μ 곡"""
return f"""
<!DOCTYPE html>
<html>
<head>
<title>API λ¬Έμ</title>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@3/swagger-ui.css">
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@3/swagger-ui-bundle.js"></script>
<script>
const ui = SwaggerUIBundle({{
url: '/swagger.json',
dom_id: '#swagger-ui',
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIBundle.SwaggerUIStandalonePreset
],
layout: "BaseLayout",
deepLinking: true
}})
</script>
</body>
</html>
"""
# λ¬Έμν λꡬμ μ½λ μμ± μμ
def generate_client_code(spec_path: str, language: str) -> str:
"""
OpenAPI μ€νμμ ν΄λΌμ΄μΈνΈ μ½λ μμ± (μμ)
Args:
spec_path: OpenAPI μ€ν νμΌ κ²½λ‘
language: μμ±ν μΈμ΄ (python, javascript, etc.)
Returns:
str: μμ±λ μ½λ μ μ₯ κ²½λ‘
"""
# μ€μ ꡬνμμλ λꡬ νΈμΆ (μ: OpenAPI Generator)
supported = ["python", "javascript", "typescript", "java", "csharp"]
if language not in supported:
raise ValueError(f"μ§μνμ§ μλ μΈμ΄: {language}. μ§μ μΈμ΄: {', '.join(supported)}")
# μμ μ½λ (μ€μ ꡬνμ λꡬ μ¬μ©)
print(f"{language} ν΄λΌμ΄μΈνΈ μ½λ μμ± μ€...")
output_path = f"./generated-clients/{language}-client"
# μ€μ ꡬν:
# subprocess.run(["openapi-generator", "generate", "-i", spec_path,
# "-g", language, "-o", output_path])
return output_path
β
νΉμ§:
- OpenAPI/Swagger λͺ μΈλ₯Ό ν΅ν λ¬Έμν
- apispecμ νμ©ν λμ λ¬Έμ μμ±
- Marshmallow μ€ν€λ§μ ν΅ν©λ λ°μ΄ν° κ²μ¦
- Swagger UIλ₯Ό ν΅ν λνν λ¬Έμ
- λνλ©ν μ΄μ μ£Όμ νμ©
- μλνλ ν΄λΌμ΄μΈνΈ μ½λ μμ±
- μμ²/μλ΅ μ€ν€λ§ μ μ
- API μλν¬μΈνΈ λ©νλ°μ΄ν° μ μ
- μ½λμ λ¬Έμμ μΌκ΄μ± μ μ§
- λ¬Έμ μλ μ λ°μ΄νΈ λ©μ»€λμ¦
β
λͺ¨λ² μ¬λ‘:
-
리μμ€ μ€μ¬ μ€κ³: URIλ λͺ
μ¬ν 리μμ€λ₯Ό λνλ΄λ©°, HTTP λ©μλλ λμμ λνλ΄λλ‘ μ€κ³νλ€. (μ:
/users
,/articles
) - μΌκ΄λ λͺ λͺ κ·μΉ: URI κ²½λ‘, 쿼리 νλΌλ―Έν°, μλ΅ νλμ μΌκ΄λ λͺ λͺ κ·μΉμ μ μ©νλ€. (camelCase λλ snake_case μ€ νλλ₯Ό μ ννμ¬ ν΅μΌ)
- μ μ ν HTTP μν μ½λ: κ° μλ΅μ μ ν©ν HTTP μν μ½λλ₯Ό μ¬μ©νλ€. (200 OK, 201 Created, 400 Bad Request, 401 Unauthorized, 404 Not Found λ±)
- μμ² κ²μ¦: λͺ¨λ API μ λ ₯κ°μ κ²μ¦νκ³ μ ν¨νμ§ μμ κ²½μ° λͺ νν μ€λ₯ λ©μμ§λ₯Ό λ°ννλ€.
- μλ΅ νμ νμ€ν: λͺ¨λ API μλ΅μ μΌκ΄λ νμμ μ¬μ©νμ¬ ν΄λΌμ΄μΈνΈκ° μ½κ² μ²λ¦¬ν μ μλλ‘ νλ€.
- νμ΄μ§λ€μ΄μ ꡬν: λλμ λ°μ΄ν°λ₯Ό λ°ννλ APIμλ νμ΄μ§λ€μ΄μ μ μ μ©νκ³ , λ§ν¬ ν€λλ λ©νλ°μ΄ν°λ₯Ό ν΅ν΄ νμ μ 보λ₯Ό μ 곡νλ€.
- λΆλΆ μλ΅ μ§μ: ν΄λΌμ΄μΈνΈκ° νμν νλλ§ μμ²ν μ μλ λ©μ»€λμ¦μ μ 곡νμ¬ λ€νΈμν¬ λΆνλ₯Ό μ€μΈλ€.
- API λ²μ κ΄λ¦¬: νΈνμ±μ κΉ¨λ λ³κ²½μ¬νμ΄ μμ λ λ²μ μ λͺ νν κ΄λ¦¬νκ³ , μ΄μ λ²μ μ λν μ§μ μ μ± μ λ¬Έμννλ€.
- CORS μ€μ : μΉ μ ν리μΌμ΄μ μ΄ APIλ₯Ό μ¬μ©ν μ μλλ‘ μ μ ν CORS(Cross-Origin Resource Sharing) ν€λλ₯Ό μ€μ νλ€.
- μ μ ν μΈμ¦ λ©μ»€λμ¦: API μ©λμ 보μ μꡬμ¬νμ λ§λ μΈμ¦ λ©μ»€λμ¦μ μ ννλ€. (JWT, OAuth 2.0, API ν€ λ±)
- μλ μ ν ꡬν: μλΉμ€ λ¨μ©μ λ°©μ§νκΈ° μν΄ ν΄λΌμ΄μΈνΈλ³ μμ² μλ μ νμ ꡬννκ³ , μ ν μ 보λ₯Ό ν€λμ ν¬ν¨νλ€.
-
μΊμ± μ λ΅:
Cache-Control
,ETag
λ±μ ν€λλ₯Ό μ¬μ©νμ¬ ν¨μ¨μ μΈ μΊμ±μ ꡬννκ³ λΆνμν μμ²μ μ€μΈλ€. - HATEOAS κ³ λ €: νμν κ²½μ° μλ΅μ κ΄λ ¨ 리μμ€ λ§ν¬λ₯Ό ν¬ν¨νμ¬ API νμμ±μ ν₯μμν¨λ€. (Hypermedia as the Engine of Application State)
- ν¨μ¨μ μΈ λ°°μΉ μμ : λ€μμ 리μμ€λ₯Ό μ²λ¦¬ν΄μΌ νλ κ²½μ° λ°°μΉ μμ μ μ§μνμ¬ λ€νΈμν¬ μμ² μλ₯Ό μ€μΈλ€.
- λΉλκΈ° μμ μ²λ¦¬: μκ°μ΄ μ€λ 걸리λ μμ μ λΉλκΈ°λ‘ μ²λ¦¬νκ³ μν νμΈ μλν¬μΈνΈλ₯Ό μ 곡νλ€.
- μμΈν λ‘κΉ : API μμ²κ³Ό μλ΅μ μ μ ν λ‘κΉ νμ¬ λλ²κΉ κ³Ό λͺ¨λν°λ§μ μ©μ΄νκ² νλ€.
- λ¬Έμν: λͺ¨λ API μλν¬μΈνΈ, νλΌλ―Έν°, μλ΅ νμ, μ€λ₯ μ½λλ₯Ό μμΈν λ¬Έμννλ€.
- API ν¬μ€μ²΄ν¬ μλν¬μΈνΈ: API μνλ₯Ό λͺ¨λν°λ§ν μ μλ ν¬μ€μ²΄ν¬ μλν¬μΈνΈλ₯Ό μ 곡νλ€.
-
보μ ν€λ μ€μ :
X-Content-Type-Options
,X-Frame-Options
,Content-Security-Policy
λ±μ 보μ ν€λλ₯Ό μ€μ νλ€.