KR_REST - somaz94/python-study GitHub Wiki

Python REST API κ°œλ… 정리


1️⃣ REST API 기초

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) 톡신


2️⃣ REST API ν΄λΌμ΄μ–ΈνŠΈ

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 λ³€ν™˜ μžλ™ν™”
  • νƒ€μž… νžŒνŒ…μ„ ν†΅ν•œ μ½”λ“œ 가독성 ν–₯상
  • λ¦¬μ†ŒμŠ€ 정리λ₯Ό μœ„ν•œ μ†Œλ©Έμž κ΅¬ν˜„


3️⃣ API 인증과 λ³΄μ•ˆ

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 헀더λ₯Ό ν™œμš©ν•œ 인증 정보 전달
  • λ³΄μ•ˆ 헀더 μ„€μ •
  • λΉ„λ°€λ²ˆν˜Έ ν•΄μ‹± 처리


4️⃣ API 버전 관리

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 버전 정보 및 지원 μ •μ±… 제곡
  • 버전별 κΈ°λŠ₯ 차이 λͺ…μ‹œ
  • 버전 지원 μ’…λ£Œ 일정 관리
  • νƒ€μž… νžŒνŒ…μ„ ν†΅ν•œ μ½”λ“œ 가독성 ν–₯상
  • λ‘œκΉ…μ„ ν†΅ν•œ 버전 μš”μ²­ 좔적
  • μ§€μ›ν•˜μ§€ μ•ŠλŠ” 버전에 λŒ€ν•œ 였λ₯˜ 처리
  • κΈ°λ³Έ 버전 μ„€μ •μœΌλ‘œ ν•˜μœ„ ν˜Έν™˜μ„± μœ μ§€


5️⃣ 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 λ“±μ˜ λ³΄μ•ˆ 헀더λ₯Ό μ„€μ •ν•œλ‹€.


⚠️ **GitHub.com Fallback** ⚠️