# Gateway 프록시 패턴 아키텍처 ## 작성일: 2025-08-21 ## 작성자: Claude (51123 서버 관리자) --- ## 1. 개요 robeing-gateway (포트 8100)의 프록시 패턴 및 JWT 인증 처리 아키텍처 문서입니다. Gateway는 모든 API 요청의 진입점으로서 인증, 라우팅, UUID 변환을 담당합니다. --- ## 2. 핵심 역할 ### 2.1 주요 기능 - **JWT 토큰 검증**: 내부적으로 토큰 유효성 검증 - **Username → UUID 변환**: JWT의 username을 UUID로 변환 - **요청 라우팅**: 적절한 백엔드 서비스로 프록시 - **헤더 주입**: X-User-Id, X-Username 헤더 추가 - **보안 게이트웨이**: 인증되지 않은 요청 차단 ### 2.2 서비스 위치 - **서버**: 51123 - **포트**: 8100 - **컨테이너**: robeing-gateway --- ## 3. 인증 플로우 ### 3.1 JWT 검증 프로세스 ```mermaid sequenceDiagram participant Client as 클라이언트 participant Gateway as Gateway(8100) participant DB as PostgreSQL participant Service as 백엔드 서비스 Client->>Gateway: API 요청 + JWT Token Gateway->>Gateway: JWT 서명 검증 Note over Gateway: HS256 알고리즘 alt 토큰 무효 Gateway-->>Client: 401 Unauthorized else 토큰 유효 Gateway->>Gateway: username 추출 Note over Gateway: JWT payload에서 Gateway->>DB: SELECT id FROM user WHERE username = ? DB-->>Gateway: UUID Gateway->>Service: 요청 전달 Note over Service: Headers:
X-User-Id: UUID
X-Username: username Service-->>Gateway: 응답 Gateway-->>Client: 응답 전달 end ``` ### 3.2 JWT 구조 ```json { "header": { "alg": "HS256", "typ": "JWT" }, "payload": { "username": "happybell80", "email": "goeun2dc@gmail.com", "exp": 1724356800, "iat": 1724270400 } } ``` --- ## 4. UUID 변환 메커니즘 ### 4.1 변환 로직 ```python class GatewayProxy: async def convert_username_to_uuid(self, username: str) -> str: """Username을 UUID로 변환""" query = "SELECT id FROM user WHERE username = $1" result = await self.db.fetchone(query, username) if not result: raise UserNotFoundError(f"User {username} not found") return str(result['id']) async def process_request(self, request): # 1. JWT에서 username 추출 token = request.headers.get('Authorization') payload = self.verify_jwt(token) username = payload['username'] # 2. UUID 변환 user_uuid = await self.convert_username_to_uuid(username) # 3. 헤더 추가 request.headers['X-User-Id'] = user_uuid request.headers['X-Username'] = username # 4. 프록시 return await self.proxy_to_service(request) ``` ### 4.2 캐싱 전략 ```python from functools import lru_cache from datetime import datetime, timedelta class UserCache: def __init__(self, ttl_seconds=300): self.cache = {} self.ttl = timedelta(seconds=ttl_seconds) async def get_uuid(self, username: str) -> str: # 캐시 확인 if username in self.cache: entry = self.cache[username] if datetime.now() < entry['expires']: return entry['uuid'] # DB 조회 uuid = await self.fetch_from_db(username) # 캐시 저장 self.cache[username] = { 'uuid': uuid, 'expires': datetime.now() + self.ttl } return uuid ``` --- ## 5. 라우팅 규칙 ### 5.1 서비스 매핑 ```yaml routes: # 인증 서비스 - path: /api/auth/* service: auth-server host: localhost port: 9000 auth_required: false # Gmail 아이템 - path: /api/items/gmail/* service: robeing-monitor host: localhost port: 9024 auth_required: true # 로빙 서비스 (51124 서버) - path: /api/robeing/* service: rb10508_micro host: 192.168.219.52 port: 10508 auth_required: true # 스킬 서비스 - path: /api/skills/email/* service: skill-email host: 192.168.219.52 port: 8501 auth_required: true ``` ### 5.2 동적 라우팅 ```python class DynamicRouter: def __init__(self): self.routes = self.load_routes() async def route_request(self, path: str, headers: dict): # 경로 매칭 for route in self.routes: if self.match_path(path, route['path']): # 인증 확인 if route['auth_required'] and not headers.get('X-User-Id'): raise AuthenticationError() # 프록시 return await self.proxy( host=route['host'], port=route['port'], path=path, headers=headers ) raise NotFoundError(f"No route for {path}") ``` --- ## 6. 에러 처리 ### 6.1 에러 응답 포맷 ```json { "error": { "code": "AUTHENTICATION_FAILED", "message": "Invalid or expired token", "timestamp": "2025-08-21T10:30:00Z", "request_id": "req_123456" } } ``` ### 6.2 에러 코드 | 코드 | HTTP Status | 설명 | |------|-------------|------| | INVALID_TOKEN | 401 | JWT 토큰 무효 | | TOKEN_EXPIRED | 401 | JWT 토큰 만료 | | USER_NOT_FOUND | 404 | Username에 해당하는 사용자 없음 | | SERVICE_UNAVAILABLE | 503 | 백엔드 서비스 응답 없음 | | RATE_LIMIT_EXCEEDED | 429 | 요청 제한 초과 | --- ## 7. 성능 최적화 ### 7.1 Connection Pooling ```python class ConnectionPool: def __init__(self): self.pools = {} async def get_connection(self, service: str): if service not in self.pools: self.pools[service] = await aiohttp.ClientSession( connector=aiohttp.TCPConnector( limit=100, limit_per_host=30, ttl_dns_cache=300 ) ) return self.pools[service] ``` ### 7.2 Request/Response 압축 ```python @app.middleware("http") async def compression_middleware(request: Request, call_next): response = await call_next(request) # gzip 압축 적용 if "gzip" in request.headers.get("Accept-Encoding", ""): response.headers["Content-Encoding"] = "gzip" response.body = gzip.compress(response.body) return response ``` --- ## 8. 모니터링 ### 8.1 메트릭 수집 ```python metrics = { "request_count": Counter("gateway_requests_total"), "request_duration": Histogram("gateway_request_duration_seconds"), "auth_failures": Counter("gateway_auth_failures_total"), "proxy_errors": Counter("gateway_proxy_errors_total") } @app.middleware("http") async def metrics_middleware(request: Request, call_next): start_time = time.time() try: response = await call_next(request) metrics["request_count"].inc() return response except Exception as e: metrics["proxy_errors"].inc() raise finally: duration = time.time() - start_time metrics["request_duration"].observe(duration) ``` ### 8.2 로그 포맷 ```json { "timestamp": "2025-08-21T10:30:00Z", "level": "INFO", "service": "gateway", "request_id": "req_123456", "method": "POST", "path": "/api/robeing/chat", "username": "happybell80", "user_id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "backend_service": "rb10508_micro", "response_time_ms": 150, "status_code": 200 } ``` --- ## 9. 보안 고려사항 ### 9.1 Rate Limiting ```python from datetime import datetime, timedelta class RateLimiter: def __init__(self, requests_per_minute=60): self.limits = {} self.max_requests = requests_per_minute async def check_limit(self, user_id: str) -> bool: now = datetime.now() minute_ago = now - timedelta(minutes=1) # 사용자별 요청 기록 if user_id not in self.limits: self.limits[user_id] = [] # 1분 이내 요청만 유지 self.limits[user_id] = [ ts for ts in self.limits[user_id] if ts > minute_ago ] # 제한 확인 if len(self.limits[user_id]) >= self.max_requests: return False self.limits[user_id].append(now) return True ``` ### 9.2 CORS 설정 ```python from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins=["https://app.ro-being.com"], allow_credentials=True, allow_methods=["GET", "POST", "PUT", "DELETE"], allow_headers=["Authorization", "Content-Type"], max_age=3600 ) ``` --- ## 10. 구현 예제 ### 10.1 완전한 Gateway 클래스 ```python from fastapi import FastAPI, Request, HTTPException import httpx import jwt import asyncpg class RobeingGateway: def __init__(self): self.app = FastAPI() self.db_pool = None self.http_client = httpx.AsyncClient() self.setup_routes() async def startup(self): self.db_pool = await asyncpg.create_pool( "postgresql://robeings:robeings@localhost/main_db" ) def setup_routes(self): @self.app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"]) async def proxy(request: Request, path: str): # JWT 검증 token = request.headers.get("Authorization", "").replace("Bearer ", "") if not token and self.requires_auth(path): raise HTTPException(401, "Missing token") # Username → UUID 변환 if token: payload = self.verify_jwt(token) username = payload['username'] user_uuid = await self.get_user_uuid(username) # 헤더 추가 headers = dict(request.headers) headers['X-User-Id'] = user_uuid headers['X-Username'] = username else: headers = dict(request.headers) # 백엔드 서비스로 프록시 backend_url = self.get_backend_url(path) response = await self.http_client.request( method=request.method, url=backend_url, headers=headers, content=await request.body() ) return response.json() def verify_jwt(self, token: str) -> dict: try: return jwt.decode( token, "your-secret-key", algorithms=["HS256"] ) except jwt.InvalidTokenError: raise HTTPException(401, "Invalid token") async def get_user_uuid(self, username: str) -> str: async with self.db_pool.acquire() as conn: row = await conn.fetchrow( "SELECT id FROM user WHERE username = $1", username ) if not row: raise HTTPException(404, f"User {username} not found") return str(row['id']) ``` --- ## 11. 테스트 전략 ### 11.1 단위 테스트 ```python import pytest from unittest.mock import Mock, patch class TestGateway: @pytest.mark.asyncio async def test_username_to_uuid_conversion(self): gateway = RobeingGateway() # Mock DB response with patch.object(gateway, 'db_pool') as mock_pool: mock_pool.acquire.return_value.__aenter__.return_value.fetchrow.return_value = { 'id': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' } uuid = await gateway.get_user_uuid('happybell80') assert uuid == 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' def test_jwt_verification(self): gateway = RobeingGateway() # Valid token token = jwt.encode( {'username': 'test', 'exp': datetime.now() + timedelta(hours=1)}, 'your-secret-key', algorithm='HS256' ) payload = gateway.verify_jwt(token) assert payload['username'] == 'test' ``` --- **문서 끝**