diff --git a/services/press-defect-data-simulator-service/app/config/__init__.py b/services/press-defect-data-simulator-service/app/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/press-defect-data-simulator-service/app/config/settings.py b/services/press-defect-data-simulator-service/app/config/settings.py index 3d20ec5..70c871e 100644 --- a/services/press-defect-data-simulator-service/app/config/settings.py +++ b/services/press-defect-data-simulator-service/app/config/settings.py @@ -19,7 +19,13 @@ class Settings(BaseSettings): azure_container_name: str = "simulator-data" azure_press_defect_path: str = "press-defect" - # 모델 서비스 설정 + # 🆕 Spring Boot 서비스 설정 (NEW!) + spring_boot_service_url: str = os.getenv("SPRING_BOOT_SERVICE_URL", "http://localhost:8092") + spring_boot_raw_data_endpoint: str = "/api/press-defect/raw-data" + spring_boot_health_endpoint: str = "/api/press-defect/health" + spring_boot_timeout: int = 300 # 5분 + + # 모델 서비스 설정 (기존 - 백업용으로 유지) model_service_url: str = os.getenv("MODEL_SERVICE_URL", "http://127.0.0.1:8000") model_service_predict_endpoint: str = "/predict/inspection" model_service_health_endpoint: str = "/health" @@ -49,12 +55,16 @@ class Settings(BaseSettings): successful_simulations: int = 0 failed_simulations: int = 0 + # 🆕 아키텍처 모드 설정 (NEW!) + architecture_mode: str = os.getenv("ARCHITECTURE_MODE", "event_driven") # "direct" or "event_driven" + # 개발/운영 모드 debug_mode: bool = os.getenv("DEBUG", "false").lower() == "true" class Config: case_sensitive = False env_file = ".env" + extra = 'ignore' # 전역 설정 인스턴스 settings = Settings() @@ -73,16 +83,27 @@ def get_settings_summary(): "press_defect_path": settings.azure_press_defect_path, "connection_configured": bool(settings.azure_connection_string) }, + "spring_boot_service": { + "url": settings.spring_boot_service_url, + "raw_data_endpoint": settings.spring_boot_raw_data_endpoint, + "timeout": settings.spring_boot_timeout, + "enabled": settings.architecture_mode == "event_driven" + }, "model_service": { "url": settings.model_service_url, "predict_endpoint": settings.model_service_predict_endpoint, - "timeout": settings.model_service_timeout + "timeout": settings.model_service_timeout, + "enabled": settings.architecture_mode == "direct" }, "scheduler": { "interval_seconds": settings.scheduler_interval_seconds, "max_inspection_count": settings.max_inspection_count, "enabled": settings.simulation_enabled }, + "architecture": { + "mode": settings.architecture_mode, + "description": "event_driven: Spring Boot + Kafka, direct: FastAPI 직접 호출" + }, "logging": { "level": settings.log_level, "directory": settings.log_dir, @@ -98,8 +119,11 @@ def validate_settings(): if not settings.azure_connection_string: errors.append("AZURE_CONNECTION_STRING이 설정되지 않았습니다.") - if not settings.model_service_url: + if settings.architecture_mode == "direct" and not settings.model_service_url: errors.append("MODEL_SERVICE_URL이 설정되지 않았습니다.") + + if settings.architecture_mode == "event_driven" and not settings.spring_boot_service_url: + errors.append("SPRING_BOOT_SERVICE_URL이 설정되지 않았습니다.") if settings.scheduler_interval_seconds < 10: errors.append("스케줄러 간격은 최소 10초 이상이어야 합니다.") @@ -107,6 +131,9 @@ def validate_settings(): if settings.max_inspection_count < 1: errors.append("inspection 개수는 최소 1개 이상이어야 합니다.") + if settings.architecture_mode not in ["direct", "event_driven"]: + errors.append("ARCHITECTURE_MODE는 'direct' 또는 'event_driven'이어야 합니다.") + return errors # 설정 업데이트 함수들 @@ -138,5 +165,6 @@ def get_simulation_stats(): "failed_simulations": settings.failed_simulations, "success_rate": round(success_rate, 2), "current_inspection_id": settings.current_inspection_id, - "simulation_enabled": settings.simulation_enabled + "simulation_enabled": settings.simulation_enabled, + "architecture_mode": settings.architecture_mode } \ No newline at end of file diff --git a/services/press-defect-data-simulator-service/app/main.py b/services/press-defect-data-simulator-service/app/main.py index 8e09e3a..2263957 100644 --- a/services/press-defect-data-simulator-service/app/main.py +++ b/services/press-defect-data-simulator-service/app/main.py @@ -8,12 +8,12 @@ import signal import sys -from config.settings import settings, validate_settings, get_settings_summary -from utils.logger import simulator_logger -from services.azure_storage import azure_storage_service -from services.model_client import model_service_client -from services.scheduler_service import scheduler_service -from routers import connection_test_router, simulator_router +from app.config.settings import settings, validate_settings, get_settings_summary +from app.utils.logger import simulator_logger +from app.services.azure_storage import azure_storage_service +from app.services.model_client import model_service_client +from app.services.scheduler_service import scheduler_service +from app.routers import connection_test_router, simulator_router # 애플리케이션 생명주기 관리 @asynccontextmanager @@ -49,7 +49,8 @@ async def lifespan(app: FastAPI): simulator_logger.logger.info("🔍 외부 서비스 연결 확인 중...") # Azure Storage 연결 테스트 - azure_available = await azure_storage_service.test_connection() + azure_available = await azure_storage_service.initialize() # 먼저 초기화 + azure_available = await azure_storage_service.test_connection() # 그다음 연결 테스트 if azure_available: simulator_logger.logger.info("✅ Azure Storage 연결 확인 완료") else: diff --git a/services/press-defect-data-simulator-service/app/routers/__init__.py b/services/press-defect-data-simulator-service/app/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/press-defect-data-simulator-service/app/routers/connection_test_router.py b/services/press-defect-data-simulator-service/app/routers/connection_test_router.py index 96c5565..033b9a2 100644 --- a/services/press-defect-data-simulator-service/app/routers/connection_test_router.py +++ b/services/press-defect-data-simulator-service/app/routers/connection_test_router.py @@ -2,9 +2,9 @@ from typing import Dict, Any import asyncio -from services.azure_storage import azure_storage_service -from services.model_client import model_service_client -from utils.logger import simulator_logger +from app.services.azure_storage import azure_storage_service +from app.services.model_client import model_service_client +from app.utils.logger import simulator_logger router = APIRouter(prefix="/connection", tags=["Connection Test"]) diff --git a/services/press-defect-data-simulator-service/app/routers/simulator_router.py b/services/press-defect-data-simulator-service/app/routers/simulator_router.py index 9789a54..c7b1886 100644 --- a/services/press-defect-data-simulator-service/app/routers/simulator_router.py +++ b/services/press-defect-data-simulator-service/app/routers/simulator_router.py @@ -2,9 +2,9 @@ from typing import Dict, Any, Optional, List from pydantic import BaseModel -from services.scheduler_service import scheduler_service -from config.settings import get_simulation_stats, get_settings_summary -from utils.logger import simulator_logger +from app.services.scheduler_service import scheduler_service +from app.config.settings import get_simulation_stats, get_settings_summary +from app.utils.logger import simulator_logger router = APIRouter(prefix="/simulator", tags=["Simulator Control"]) diff --git a/services/press-defect-data-simulator-service/app/services/__init__.py b/services/press-defect-data-simulator-service/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/press-defect-data-simulator-service/app/services/azure_storage.py b/services/press-defect-data-simulator-service/app/services/azure_storage.py index 36dadb8..517ce1b 100644 --- a/services/press-defect-data-simulator-service/app/services/azure_storage.py +++ b/services/press-defect-data-simulator-service/app/services/azure_storage.py @@ -6,8 +6,8 @@ import asyncio from PIL import Image -from config.settings import settings -from utils.logger import simulator_logger +from app.config.settings import settings +from app.utils.logger import simulator_logger class AzureStorageService: """Azure Blob Storage 서비스""" diff --git a/services/press-defect-data-simulator-service/app/services/model_client.py b/services/press-defect-data-simulator-service/app/services/model_client.py index 71c8c6e..1299173 100644 --- a/services/press-defect-data-simulator-service/app/services/model_client.py +++ b/services/press-defect-data-simulator-service/app/services/model_client.py @@ -4,8 +4,8 @@ from typing import Dict, List, Optional, Tuple, Any import json -from config.settings import settings -from utils.logger import simulator_logger +from app.config.settings import settings +from app.utils.logger import simulator_logger class ModelServiceClient: """FastAPI 모델 서비스 클라이언트""" diff --git a/services/press-defect-data-simulator-service/app/services/scheduler_service.py b/services/press-defect-data-simulator-service/app/services/scheduler_service.py index d0f03b9..95483c9 100644 --- a/services/press-defect-data-simulator-service/app/services/scheduler_service.py +++ b/services/press-defect-data-simulator-service/app/services/scheduler_service.py @@ -5,13 +5,14 @@ from typing import Dict, Any, Optional import schedule -from config.settings import settings, update_simulation_status, increment_simulation_stats, update_current_inspection_id, get_simulation_stats -from utils.logger import simulator_logger -from services.azure_storage import azure_storage_service -from services.model_client import model_service_client +from app.config.settings import settings, update_simulation_status, increment_simulation_stats, update_current_inspection_id, get_simulation_stats +from app.utils.logger import simulator_logger +from app.services.azure_storage import azure_storage_service +from app.services.model_client import model_service_client # 기존 (백업용) +from app.services.spring_boot_client import spring_boot_client # 🆕 새로 추가 class SchedulerService: - """스케줄러 서비스 - 1분마다 데이터 수집 및 예측""" + """스케줄러 서비스 - Event Driven Architecture 지원""" def __init__(self): self.running = False @@ -23,7 +24,8 @@ def __init__(self): # 서비스 상태 self.azure_ready = False - self.model_ready = False + self.model_ready = False # 기존 FastAPI 모델 서비스 + self.spring_boot_ready = False # 🆕 Spring Boot 서비스 self.initialization_completed = False # 통계 @@ -36,7 +38,7 @@ async def initialize_services(self) -> bool: try: simulator_logger.log_scheduler_event("서비스 초기화 시작") - # 1. Azure Storage 초기화 + # 1. Azure Storage 초기화 (공통) simulator_logger.logger.info("🔄 Azure Storage 초기화 중...") self.azure_ready = await azure_storage_service.initialize() @@ -44,26 +46,53 @@ async def initialize_services(self) -> bool: simulator_logger.log_scheduler_event("Azure Storage 초기화 실패") return False - # 2. Model Service 연결 확인 - simulator_logger.logger.info("🔄 Model Service 연결 확인 중...") - self.model_ready = await model_service_client.test_connection() - - if not self.model_ready: - simulator_logger.log_scheduler_event("Model Service 연결 실패") - return False - - # 3. 모델 준비 상태 확인 - ready_result = await model_service_client.check_model_ready() - if ready_result['status'] != 'ready': - simulator_logger.log_scheduler_event( - "Model Service 준비 안됨", - {"status": ready_result['status'], "error": ready_result.get('error')} - ) - self.model_ready = False - return False + # 2. 아키텍처 모드에 따른 서비스 초기화 + if settings.architecture_mode == "event_driven": + # 🆕 Event Driven: Spring Boot 서비스 연결 확인 + simulator_logger.logger.info("🔄 Spring Boot 서비스 연결 확인 중... (Event Driven Mode)") + self.spring_boot_ready = await spring_boot_client.test_connection() + + if not self.spring_boot_ready: + simulator_logger.log_scheduler_event("Spring Boot 서비스 연결 실패") + return False + + # Spring Boot 서비스 상태 확인 + health_result = await spring_boot_client.check_service_health() + if health_result['status'] != 'healthy': + simulator_logger.log_scheduler_event( + "Spring Boot 서비스 준비 안됨", + {"status": health_result['status'], "error": health_result.get('error')} + ) + self.spring_boot_ready = False + return False + + simulator_logger.logger.info("✅ Event Driven 아키텍처로 초기화 완료 (Spring Boot + Kafka)") + + else: + # 기존: Direct Call - Model Service 연결 확인 + simulator_logger.logger.info("🔄 Model Service 연결 확인 중... (Direct Call Mode)") + self.model_ready = await model_service_client.test_connection() + + if not self.model_ready: + simulator_logger.log_scheduler_event("Model Service 연결 실패") + return False + + # 모델 준비 상태 확인 + ready_result = await model_service_client.check_model_ready() + if ready_result['status'] != 'ready': + simulator_logger.log_scheduler_event( + "Model Service 준비 안됨", + {"status": ready_result['status'], "error": ready_result.get('error')} + ) + self.model_ready = False + return False + + simulator_logger.logger.info("✅ Direct Call 아키텍처로 초기화 완료 (FastAPI 직접 호출)") self.initialization_completed = True - simulator_logger.log_scheduler_event("모든 서비스 초기화 완료") + simulator_logger.log_scheduler_event( + f"모든 서비스 초기화 완료 (모드: {settings.architecture_mode})" + ) return True @@ -72,7 +101,7 @@ async def initialize_services(self) -> bool: return False async def execute_single_simulation(self) -> bool: - """단일 시뮬레이션 실행""" + """단일 시뮬레이션 실행 - 아키텍처 모드별 분기""" start_time = time.time() inspection_id = self.current_inspection_id @@ -82,7 +111,7 @@ async def execute_single_simulation(self) -> bool: simulator_logger.log_simulation_start(inspection_id) - # 1. Azure Storage에서 이미지 다운로드 + # 1. Azure Storage에서 이미지 다운로드 (공통) download_success, images_data = await azure_storage_service.download_inspection_images(inspection_id) if not download_success or not images_data: @@ -92,9 +121,91 @@ async def execute_single_simulation(self) -> bool: increment_simulation_stats(success=False) return False - # 2. 모델 서비스에 예측 요청 + # 2. 아키텍처 모드에 따른 분기 처리 + if settings.architecture_mode == "event_driven": + # 🆕 Event Driven: Spring Boot로 원시 데이터 전송 + success = await self._execute_event_driven_simulation(inspection_id, images_data, start_time) + else: + # 기존: Direct Call - FastAPI 모델 서비스 직접 호출 + success = await self._execute_direct_call_simulation(inspection_id, images_data, start_time) + + return success + + except Exception as e: + processing_time = time.time() - start_time + error_msg = f"시뮬레이션 실행 중 예외 발생: {str(e)}" + simulator_logger.log_simulation_failure(inspection_id, error_msg, processing_time) + increment_simulation_stats(success=False) + return False + + finally: + # 다음 inspection ID로 이동 (순환) + self.current_inspection_id += 1 + if self.current_inspection_id > settings.max_inspection_count: + self.current_inspection_id = settings.start_inspection_id + simulator_logger.log_scheduler_event( + "Inspection ID 순환 완료", + {"next_id": self.current_inspection_id} + ) + + async def _execute_event_driven_simulation(self, inspection_id: int, images_data: list, start_time: float) -> bool: + """🆕 Event Driven 방식 시뮬레이션 실행""" + try: + inspection_id_str = f"inspection_{inspection_id:03d}" + + # Spring Boot로 원시 데이터 전송 (이후 Kafka를 통해 모델 서비스로 전달됨) + transmission_success, result_data, error_msg = await spring_boot_client.send_raw_data( + inspection_id=inspection_id_str, + images=images_data + ) + + processing_time = time.time() - start_time + + if transmission_success and result_data: + # Spring Boot 전송 성공 (Event Driven에서는 이것이 성공 기준) + simulator_logger.logger.info( + f"✅ Event Driven 시뮬레이션 완료: {inspection_id_str} - Spring Boot 전송 성공 ({processing_time:.2f}초)" + ) + + # 통계 업데이트 + increment_simulation_stats(success=True) + self.total_processing_time += processing_time + self.execution_count += 1 + self.average_processing_time = self.total_processing_time / self.execution_count + + # 성공 로그 (Event Driven에서는 Spring Boot 응답을 기준으로) + simulator_logger.log_simulation_success( + inspection_id, + { + "final_judgment": { + "quality_status": "전송완료", + "recommendation": "Processing" + }, + "event_driven_response": result_data + }, + processing_time + ) + + return True + else: + # Spring Boot 전송 실패 + simulator_logger.log_simulation_failure(inspection_id, error_msg, processing_time) + increment_simulation_stats(success=False) + return False + + except Exception as e: + processing_time = time.time() - start_time + error_msg = f"Event Driven 시뮬레이션 실행 중 오류: {str(e)}" + simulator_logger.log_simulation_failure(inspection_id, error_msg, processing_time) + increment_simulation_stats(success=False) + return False + + async def _execute_direct_call_simulation(self, inspection_id: int, images_data: list, start_time: float) -> bool: + """기존 Direct Call 방식 시뮬레이션 실행""" + try: inspection_id_str = f"inspection_{inspection_id:03d}" + # 기존 방식: 모델 서비스에 직접 예측 요청 prediction_success, result_data, error_msg = await model_service_client.predict_inspection( inspection_id=inspection_id_str, images=images_data @@ -121,23 +232,13 @@ async def execute_single_simulation(self) -> bool: simulator_logger.log_simulation_failure(inspection_id, error_msg, processing_time) increment_simulation_stats(success=False) return False - + except Exception as e: processing_time = time.time() - start_time - error_msg = f"시뮬레이션 실행 중 예외 발생: {str(e)}" + error_msg = f"Direct Call 시뮬레이션 실행 중 오류: {str(e)}" simulator_logger.log_simulation_failure(inspection_id, error_msg, processing_time) increment_simulation_stats(success=False) return False - - finally: - # 다음 inspection ID로 이동 (순환) - self.current_inspection_id += 1 - if self.current_inspection_id > settings.max_inspection_count: - self.current_inspection_id = settings.start_inspection_id - simulator_logger.log_scheduler_event( - "Inspection ID 순환 완료", - {"next_id": self.current_inspection_id} - ) def _schedule_job(self): """스케줄 작업 실행 (동기 함수)""" @@ -205,7 +306,8 @@ async def start_scheduler(self) -> bool: { "interval": settings.scheduler_interval_seconds, "max_inspection": settings.max_inspection_count, - "start_inspection": self.current_inspection_id + "start_inspection": self.current_inspection_id, + "architecture_mode": settings.architecture_mode } ) @@ -281,7 +383,8 @@ async def manual_execution(self) -> Dict[str, Any]: return { "success": success, "inspection_id": self.current_inspection_id - 1, # 실행 후 증가했으므로 -1 - "timestamp": datetime.now().isoformat() + "timestamp": datetime.now().isoformat(), + "architecture_mode": settings.architecture_mode } finally: @@ -306,11 +409,13 @@ def get_scheduler_status(self) -> Dict[str, Any]: "last_execution": self.last_execution_time.isoformat() if self.last_execution_time else None, "next_execution": self.next_execution_time.isoformat() if self.next_execution_time else None, "execution_count": self.execution_count, - "average_processing_time": round(self.average_processing_time, 2) + "average_processing_time": round(self.average_processing_time, 2), + "architecture_mode": settings.architecture_mode }, "service_status": { "azure_ready": self.azure_ready, - "model_ready": self.model_ready + "model_ready": self.model_ready, # 기존 FastAPI + "spring_boot_ready": self.spring_boot_ready # 🆕 Spring Boot }, "simulation_stats": simulation_stats, "settings": { @@ -330,25 +435,40 @@ async def health_check(self) -> Dict[str, Any]: azure_test = await azure_storage_service.test_connection() self.azure_ready = azure_test - if self.model_ready: - model_test = await model_service_client.test_connection() - self.model_ready = model_test - - # 전반적인 건강 상태 - overall_healthy = self.azure_ready and self.model_ready + # 아키텍처 모드별 서비스 상태 확인 + if settings.architecture_mode == "event_driven": + if self.spring_boot_ready: + spring_boot_test = await spring_boot_client.test_connection() + self.spring_boot_ready = spring_boot_test + + overall_healthy = self.azure_ready and self.spring_boot_ready + + else: # direct mode + if self.model_ready: + model_test = await model_service_client.test_connection() + self.model_ready = model_test + + overall_healthy = self.azure_ready and self.model_ready if self.running and not overall_healthy: simulator_logger.log_scheduler_event( "헬스체크 경고", - {"azure_ready": self.azure_ready, "model_ready": self.model_ready} + { + "azure_ready": self.azure_ready, + "model_ready": self.model_ready, + "spring_boot_ready": self.spring_boot_ready, + "architecture_mode": settings.architecture_mode + } ) return { "healthy": overall_healthy, "azure_storage": self.azure_ready, "model_service": self.model_ready, + "spring_boot_service": self.spring_boot_ready, "scheduler_running": self.running, "initialization_completed": self.initialization_completed, + "architecture_mode": settings.architecture_mode, "last_check": datetime.now().isoformat() } @@ -356,6 +476,7 @@ async def health_check(self) -> Dict[str, Any]: return { "healthy": False, "error": str(e), + "architecture_mode": settings.architecture_mode, "last_check": datetime.now().isoformat() } diff --git a/services/press-defect-data-simulator-service/app/services/spring_boot_client.py b/services/press-defect-data-simulator-service/app/services/spring_boot_client.py new file mode 100644 index 0000000..86e20dd --- /dev/null +++ b/services/press-defect-data-simulator-service/app/services/spring_boot_client.py @@ -0,0 +1,222 @@ +import aiohttp +import asyncio +import time +from typing import Dict, List, Optional, Tuple, Any +import json + +from app.config.settings import settings +from app.utils.logger import simulator_logger + +class SpringBootServiceClient: + """Spring Boot 서비스 클라이언트 - Event Driven Architecture""" + + def __init__(self): + self.base_url = settings.spring_boot_service_url.rstrip('/') + self.raw_data_endpoint = settings.spring_boot_raw_data_endpoint + self.health_endpoint = settings.spring_boot_health_endpoint + self.timeout = settings.spring_boot_timeout + self.retries = settings.http_retries + self.retry_delay = settings.http_retry_delay + + # 연결 상태 + self.service_available = False + self.last_health_check = None + + async def test_connection(self) -> bool: + """Spring Boot 서비스 연결 테스트""" + try: + health_url = f"{self.base_url}{self.health_endpoint}" + + async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as session: + async with session.get(health_url) as response: + if response.status == 200: + data = await response.json() + self.service_available = True + self.last_health_check = time.time() + + simulator_logger.logger.info( + f"✅ Spring Boot 서비스 연결 성공: {self.base_url}" + ) + return True + else: + simulator_logger.logger.warning( + f"⚠️ Spring Boot 서비스 응답 오류: HTTP {response.status}" + ) + return False + + except aiohttp.ClientError as e: + simulator_logger.logger.error(f"❌ Spring Boot 서비스 연결 오류: {str(e)}") + return False + except Exception as e: + simulator_logger.logger.error(f"❌ Spring Boot 서비스 알 수 없는 오류: {str(e)}") + return False + + async def check_service_health(self) -> Dict[str, Any]: + """Spring Boot 서비스 상세 헬스체크""" + try: + health_url = f"{self.base_url}{self.health_endpoint}" + + async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as session: + start_time = time.time() + async with session.get(health_url) as response: + response_time = time.time() - start_time + + if response.status == 200: + health_data = await response.json() + + return { + 'status': 'healthy', + 'response_time': round(response_time, 3), + 'service_data': health_data, + 'timestamp': time.time() + } + else: + return { + 'status': 'unhealthy', + 'response_time': round(response_time, 3), + 'error': f"HTTP {response.status}", + 'timestamp': time.time() + } + + except Exception as e: + return { + 'status': 'unreachable', + 'error': str(e), + 'timestamp': time.time() + } + + async def send_raw_data(self, inspection_id: str, images: List[Dict[str, str]]) -> Tuple[bool, Optional[Dict[str, Any]], Optional[str]]: + # 디버깅용 로그 추가 + simulator_logger.logger.info(f"🔍 DEBUG: send_raw_data 시작 - {inspection_id}, 이미지 수: {len(images)}") + simulator_logger.logger.info(f"🔍 DEBUG: URL: {self.base_url}{self.raw_data_endpoint}") + simulator_logger.logger.info(f"🔍 DEBUG: 타임아웃: {self.timeout}초") + + """Spring Boot로 원시 데이터 전송 (Event Driven 방식)""" + + start_time = time.time() + + # 재시도 로직 포함 + for attempt in range(self.retries + 1): + try: + raw_data_url = f"{self.base_url}{self.raw_data_endpoint}" + + # 요청 데이터 구성 - Spring Boot DTO 형식에 맞춤 + request_data = { + "inspectionId": inspection_id, + "images": images, + "source": "simulator", + "clientInfo": "painting-process-data-simulator-service", + "metadata": f"attempt_{attempt + 1}" + } + + # HTTP 요청 + timeout = aiohttp.ClientTimeout(total=self.timeout) + async with aiohttp.ClientSession(timeout=timeout) as session: + + simulator_logger.logger.info( + f"📤 Spring Boot로 원시 데이터 전송: {inspection_id} ({len(images)}장) - 시도 {attempt + 1}/{self.retries + 1}" + ) + + async with session.post( + raw_data_url, + json=request_data, + headers={'Content-Type': 'application/json'} + ) as response: + + processing_time = time.time() - start_time + + if response.status == 200: + result_data = await response.json() + + simulator_logger.logger.info( + f"✅ Spring Boot 원시 데이터 전송 성공: {inspection_id} ({processing_time:.2f}초)" + ) + + return True, result_data, None + + else: + error_text = await response.text() + error_msg = f"HTTP {response.status}: {error_text}" + + simulator_logger.logger.warning( + f"⚠️ Spring Boot 응답 오류: {inspection_id} - {error_msg}" + ) + + # 5xx 에러는 재시도, 4xx 에러는 즉시 실패 + if response.status >= 500 and attempt < self.retries: + simulator_logger.logger.info(f"🔄 재시도 대기 중... ({self.retry_delay}초)") + await asyncio.sleep(self.retry_delay) + continue + else: + return False, None, error_msg + + except aiohttp.ClientTimeout: + error_msg = f"요청 시간 초과 ({self.timeout}초)" + simulator_logger.logger.warning(f"⏰ {inspection_id}: {error_msg}") + + if attempt < self.retries: + simulator_logger.logger.info(f"🔄 재시도 대기 중... ({self.retry_delay}초)") + await asyncio.sleep(self.retry_delay) + continue + else: + return False, None, error_msg + + except aiohttp.ClientError as e: + error_msg = f"네트워크 오류: {str(e)}" + simulator_logger.logger.warning(f"🌐 {inspection_id}: {error_msg}") + + if attempt < self.retries: + simulator_logger.logger.info(f"🔄 재시도 대기 중... ({self.retry_delay}초)") + await asyncio.sleep(self.retry_delay) + continue + else: + return False, None, error_msg + + except Exception as e: + error_msg = f"예상치 못한 오류: {str(e)}" + simulator_logger.logger.error(f"💥 {inspection_id}: {error_msg}") + + if attempt < self.retries: + await asyncio.sleep(self.retry_delay) + continue + else: + return False, None, error_msg + + # 모든 재시도 실패 + processing_time = time.time() - start_time + final_error = f"모든 재시도 실패 ({self.retries + 1}회, {processing_time:.2f}초)" + return False, None, final_error + + async def get_service_status(self) -> Dict[str, Any]: + """Spring Boot 서비스 상태 조회""" + try: + status_url = f"{self.base_url}/api/press-defect/status" + + async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as session: + async with session.get(status_url) as response: + if response.status == 200: + return await response.json() + else: + simulator_logger.logger.warning(f"서비스 상태 조회 실패: HTTP {response.status}") + return None + + except Exception as e: + simulator_logger.logger.error(f"서비스 상태 조회 오류: {str(e)}") + return None + + def get_client_status(self) -> Dict[str, Any]: + """클라이언트 상태 정보""" + return { + 'service_name': 'Spring Boot Service Client', + 'base_url': self.base_url, + 'raw_data_endpoint': self.raw_data_endpoint, + 'health_endpoint': self.health_endpoint, + 'timeout': self.timeout, + 'retries': self.retries, + 'service_available': self.service_available, + 'last_health_check': self.last_health_check, + 'architecture_mode': 'event_driven' + } + +# 전역 Spring Boot Service 클라이언트 인스턴스 +spring_boot_client = SpringBootServiceClient() \ No newline at end of file diff --git a/services/press-defect-data-simulator-service/app/utils/__init__.py b/services/press-defect-data-simulator-service/app/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/press-defect-data-simulator-service/app/utils/logger.py b/services/press-defect-data-simulator-service/app/utils/logger.py index b4d714c..b6a28fc 100644 --- a/services/press-defect-data-simulator-service/app/utils/logger.py +++ b/services/press-defect-data-simulator-service/app/utils/logger.py @@ -6,7 +6,7 @@ from pathlib import Path from typing import Dict, Any, Optional -from config.settings import settings +from app.config.settings import settings class SimulatorLogger: """시뮬레이터 전용 로거""" diff --git a/services/press-defect-detection-model-service/app/main.py b/services/press-defect-detection-model-service/app/main.py index c356493..17db6d9 100644 --- a/services/press-defect-detection-model-service/app/main.py +++ b/services/press-defect-detection-model-service/app/main.py @@ -7,7 +7,6 @@ import asyncio # 모델 및 서비스 import 수정 - from app.press_models.yolo_model import YOLOv7Model from app.services.inference import InferenceService from app.routers.predict import router as predict_router, set_inference_service