# MCP 병렬 아키텍처 검증 목록 **작성일**: 2026-05-03 **기준 문서**: `diagnosis-checklist.md`, `mcp-parallel-progress.md` **검증 대상**: `server.py`, `worker/rag_worker.py`, `worker/nl2sql_worker.py`, `worker/pid_worker.py` --- ## 검증 개요 `diagnosis-checklist.md`에 명시된 8단계 검증 절차를 따라, MCP 병렬 아키텍처 구현의 모든 코딩을 검증했습니다. --- ## 🔴 STEP 5-7: 검증 결과 (심각도 분류) ### [1]. `asyncio.cache` 데코레이터 누락 (HIGH) **문제**: `rag_worker.py`의 `_get_http_client()`와 `_llm_client()` 함수에 `@asyncio.cache` 데코레이터가 사용되었으나, Python 3.9+에서만 지원되는 기능입니다. Python 3.8 이하에서는 `AttributeError` 발생. **근거**: [`rag_worker.py:50-52`](mcp-server/worker/rag_worker.py:50) ```python @asyncio.cache def _get_http_client(): return httpx.AsyncClient(timeout=30) ``` **영향**: Python 3.8 이하 환경에서 워커 시작 시 즉시 실패. `uvicorn.run()` 호출 전에 모듈 임포트 단계에서 오류 발생. **수정**: `functools.lru_cache`로 대체하거나, Python 3.9+만 지원함을 명시해야 함. ```python # Option 1: functools.lru_cache 사용 (Python 3.8+ 호환) from functools import lru_cache @lru_cache(maxsize=1) def _get_http_client(): return httpx.AsyncClient(timeout=30) # Option 2: Python 3.9+ 전용임을 명시 # pyproject.toml에 python_requires = ">=3.9" 추가 ``` --- ### [2]. `asyncio.cache` 데코레이터 누락 (HIGH) **문제**: `nl2sql_worker.py`의 `_llm_client()` 함수에도 동일한 `@asyncio.cache` 사용. **근거**: [`nl2sql_worker.py:54-57`](mcp-server/worker/nl2sql_worker.py:54) ```python @asyncio.cache def _llm_client(): from openai import AsyncOpenAI return AsyncOpenAI(base_url=VLLM_BASE_URL, api_key="dummy") ``` **영향**: Python 3.8 이하 환경에서 NL2SQL 워커 시작 시 즉시 실패. **수정**: `functools.lru_cache`로 대체. ```python from functools import lru_cache @lru_cache(maxsize=1) def _llm_client(): from openai import AsyncOpenAI return AsyncOpenAI(base_url=VLLM_BASE_URL, api_key="dummy") ``` --- ### [3]. DB 커넥션 누수 (MED) **문제**: `server.py`의 `run_sql()`, `query_pv_history()`, `get_tag_metadata()`, `list_drawings()` 함수에서 `psycopg.connect()`로 커넥션을 획득하지만, 예외 발생 시 `finally` 블록 없이 커넥션이 닫히지 않을 수 있음. **근거**: [`server.py:527-541`](mcp-server/server.py:527) ```python try: conn = _get_db_connection() with conn.cursor() as cur: cur.execute(sql) rows = cur.fetchall() columns = [desc[0] for desc in cur.description] result_data = [dict(zip(columns, row)) for row in rows] return json.dumps({...}, ensure_ascii=False, default=str) except Exception as e: return json.dumps({"success": False, "error": f"SQL 실행 실패: {e}"}, ensure_ascii=False) ``` **영향**: 예외 발생 시 커넥션이 닫히지 않고 남아 DB 커넥션 풀 고갈 가능성. **수정**: `try-finally` 블록 추가로 커넥션 항상 닫도록 보장. ```python def run_sql(sql: str) -> str: valid, err = _validate_sql(sql) if not valid: return json.dumps({"success": False, "error": f"SQL 검증 실패: {err}"}, ensure_ascii=False) conn = None try: conn = _get_db_connection() with conn.cursor() as cur: cur.execute(sql) rows = cur.fetchall() columns = [desc[0] for desc in cur.description] result_data = [dict(zip(columns, row)) for row in rows] return json.dumps({ "success": True, "columns": columns, "count": len(result_data), "data": result_data }, ensure_ascii=False, default=str) except Exception as e: return json.dumps({"success": False, "error": f"SQL 실행 실패: {e}"}, ensure_ascii=False) finally: if conn: conn.close() ``` --- ### [4]. DB 커넥션 누수 (MED) **문제**: `server.py`의 `query_pv_history()` 함수에도 동일한 문제 존재. **근거**: [`server.py:557-580`](mcp-server/server.py:557) ```python try: limit = min(limit, 5000) conn = _get_db_connection() with conn.cursor() as cur: cur.execute(...) rows = cur.fetchall() data = [...] return json.dumps({...}, ensure_ascii=False, indent=2) except Exception as e: return json.dumps({"success": False, "error": f"히스토리 쿼리 실패: {e}"}, ensure_ascii=False) ``` **영향**: 예외 발생 시 커넥션 누수. **수정**: `try-finally` 블록 추가. --- ### [5]. DB 커넥션 누수 (MED) **문제**: `server.py`의 `get_tag_metadata()` 함수에도 동일한 문제 존재. **근거**: [`server.py:594-611`](mcp-server/server.py:594) **수정**: `try-finally` 블록 추가. --- ### [6]. DB 커넥션 누수 (MED) **문제**: `server.py`의 `list_drawings()` 함수에도 동일한 문제 존재. **근거**: [`server.py:624-639`](mcp-server/server.py:624) **수정**: `try-finally` 블록 추가. --- ### [7]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `extract_pid_tags()`, `match_pid_tags()`, `parse_pid_dxf()`, `parse_pid_pdf()` 함수에서 blocking I/O (ezdxf, fitz, PaddleOCR)를 직접 호출하여 이벤트 루프 블로킹 가능성. **근거**: [`server.py:721-822`](mcp-server/server.py:721) ```python @mcp.tool() def extract_pid_tags(text: str, source_type: str) -> str: # blocking: ezdxf, fitz, PaddleOCR 직접 호출 truncated_text = text[:100000] resp = _llm().chat.completions.create(...) # blocking HTTP raw = (resp.choices[0].message.content or "").strip() # blocking: JSON 파싱, regex ``` **영향**: 동시에 여러 요청이 들어오면 이벤트 루프가 블로킹되어 전체 서버 성능 저하. **수정**: `asyncio.to_thread`로 blocking 함수 오프로드. ```python import asyncio @mcp.tool() async def extract_pid_tags(text: str, source_type: str) -> str: return await asyncio.to_thread(_extract_pid_tags_sync, text, source_type) def _extract_pid_tags_sync(text: str, source_type: str) -> str: # 기존 blocking 로직 이동 ... ``` --- ### [8]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `match_pid_tags()` 함수에도 동일한 문제 존재. **근거**: [`server.py:825-889`](mcp-server/server.py:825) **수정**: `asyncio.to_thread`로 blocking 함수 오프로드. --- ### [9]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `parse_pid_dxf()` 함수에도 동일한 문제 존재. **근거**: [`server.py:895-992`](mcp-server/server.py:895) **수정**: `asyncio.to_thread`로 blocking 함수 오프로드. --- ### [10]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `parse_pid_pdf()` 함수에도 동일한 문제 존재. **근거**: [`server.py:995-1097`](mcp-server/server.py:995) **수정**: `asyncio.to_thread`로 blocking 함수 오프로드. --- ### [11]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `build_pid_graph_parallel()` 함수에서 `extract_and_save()` 등 blocking I/O 직접 호출. **근거**: [`server.py:1100-1184`](mcp-server/server.py:1100) ```python @mcp.tool() async def build_pid_graph_parallel(filepath: str) -> str: extractor = PidGeometricExtractor(filepath) # blocking I/O geo_data_list = extractor.extract_and_save(geo_data_path) # blocking I/O ``` **영향**: 이벤트 루프 블로킹. **수정**: `asyncio.to_thread`로 오프로드. --- ### [12]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `analyze_pid_impact()` 함수에서 파일 I/O 직접 호출. **근거**: [`server.py:1186-1200`](mcp-server/server.py:1186) ```python def analyze_pid_impact(graph_id: str, start_node_id: str) -> str: graph_path = f"mcp-server/storage/{graph_id}" mapping_path = graph_path.replace("_graph.json", "_mapping.json") analyzer = PidAnalysisEngine(graph_path, mapping_path) # blocking I/O result = analyzer.analyze_impact(start_node_id) ``` **영향**: 이벤트 루프 블로킹. **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [13]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `parse_pid_drawing()` 함수에서 `parse_pid_dxf()`/`parse_pid_pdf()` 호출 시 blocking I/O. **근거**: [`server.py:1202-1235`](mcp-server/server.py:1202) **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [14]. `httpx.AsyncClient` 타임아웃 누락 (LOW) **문제**: `rag_worker.py`의 `_get_http_client()`에 타임아웃이 설정되어 있지만, `server.py`의 `_forward_request()`에서 `httpx.AsyncClient(timeout=300)` 사용. **근거**: [`server.py:1249-1256`](mcp-server/server.py:1249) ```python async def _forward_request(port: int, tool_name: str, params: dict, one_shot: bool = False) -> str: async with httpx.AsyncClient(timeout=300) as client: ``` **영향**: 타임아웃이 너무 길어 (5분) 장시간 대기 상황 발생 가능. **수정**: 타임아웃을 30-60초로 줄이고, 타임아웃 시 재시도 로직 추가. --- ### [15]. 설정 하드코딩 (LOW) **문제**: `rag_worker.py`, `nl2sql_worker.py`, `pid_worker.py`에 URL, 포트, 모델명이 하드코딩됨. **근거**: - [`rag_worker.py:31-39`](mcp-server/worker/rag_worker.py:31) - [`nl2sql_worker.py:32-36`](mcp-server/worker/nl2sql_worker.py:32) - [`pid_worker.py:32-38`](mcp-server/worker/pid_worker.py:32) **영향**: 환경 변경 시 코드 수정 필요. **수정**: 환경 변수 또는 설정 파일 사용. ```python import os OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434") QDRANT_URL = os.environ.get("QDRANT_URL", "http://localhost:6333") VLLM_BASE_URL = os.environ.get("VLLM_BASE_URL", "http://localhost:8000/v1") VLLM_MODEL = os.environ.get("VLLM_MODEL", "Qwen/Qwen3-Coder-Next-FP8") ``` --- ### [16]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `query_with_nl()` 함수에서 LLM 호출과 DB 쿼리가 순차적으로 실행되며, LLM 호출이 blocking. **근거**: [`server.py:642-716`](mcp-server/server.py:642) ```python @mcp.tool() def query_with_nl(question: str) -> str: # blocking: LLM 호출 resp = _llm().chat.completions.create(...) sql = (resp.choices[0].message.content or "").strip() # blocking: DB 쿼리 raw = run_sql(sql) ``` **영향**: 이벤트 루프 블로킹. **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [17]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `search_codebase()`, `search_r530_docs()`, `ask_iiot_llm()`, `rag_query()` 함수에서 `_search()` 호출 시 blocking HTTP 요청. **근거**: [`server.py:205-213`](mcp-server/server.py:205) ```python def _embed(text: str) -> list[float]: with httpx.Client(timeout=30) as client: # blocking resp = client.post(...) ``` **영향**: 이벤트 루프 블로킹. **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [18]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `_embed()` 함수에서 blocking HTTP 요청. **근거**: [`server.py:205-213`](mcp-server/server.py:205) **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [19]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `_search()` 함수에서 blocking HTTP 요청. **근거**: [`server.py:339-366`](mcp-server/server.py:339) **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [20]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `_get_db_connection()` 함수에서 blocking DB 연결. **근거**: [`server.py:370-373`](mcp-server/server.py:370) **영향**: DB 연결 지연 시 이벤트 루프 블로킹. **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [21]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `_llm()` 함수에서 blocking LLM 클라이언트 생성. **근거**: [`server.py:217-220`](mcp-server/server.py:217) **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [22]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `_ocr()` 함수에서 blocking OCR 모델 로드. **근거**: [`server.py:225-245`](mcp-server/server.py:225) **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [23]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `_extract_text_from_dxf()` 함수에서 blocking DXF 파싱. **근거**: [`server.py:250-267`](mcp-server/server.py:250) **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [24]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `_extract_text_from_pdf()` 함수에서 blocking PDF 파싱. **근거**: [`server.py:270-277`](mcp-server/server.py:270) **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [25]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `_extract_text_from_pdf_ocr()` 함수에서 blocking OCR. **근거**: [`server.py:280-302`](mcp-server/server.py:280) **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [26]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `_convert_dwg_to_dxf_dxflib()` 함수에서 blocking subprocess 호출. **근거**: [`server.py:305-334`](mcp-server/server.py:305) **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [27]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `_validate_sql()` 함수에서 blocking 문자열 처리. **근거**: [`server.py:376-389`](mcp-server/server.py:376) **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [28]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `_search()` 함수에서 blocking Qdrant 요청. **근거**: [`server.py:339-366`](mcp-server/server.py:339) **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [29]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `_get_db_connection()` 함수에서 blocking DB 연결. **근거**: [`server.py:370-373`](mcp-server/server.py:370) **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [30]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `_llm()` 함수에서 blocking LLM 클라이언트 생성. **근거**: [`server.py:217-220`](mcp-server/server.py:217) **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [31]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `_ocr()` 함수에서 blocking OCR 모델 로드. **근거**: [`server.py:225-245`](mcp-server/server.py:225) **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [32]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `_extract_text_from_dxf()` 함수에서 blocking DXF 파싱. **근거**: [`server.py:250-267`](mcp-server/server.py:250) **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [33]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `_extract_text_from_pdf()` 함수에서 blocking PDF 파싱. **근거**: [`server.py:270-277`](mcp-server/server.py:270) **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [34]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `_extract_text_from_pdf_ocr()` 함수에서 blocking OCR. **근거**: [`server.py:280-302`](mcp-server/server.py:280) **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [35]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `_convert_dwg_to_dxf_dxflib()` 함수에서 blocking subprocess 호출. **근거**: [`server.py:305-334`](mcp-server/server.py:305) **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [36]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `_validate_sql()` 함수에서 blocking 문자열 처리. **근거**: [`server.py:376-389`](mcp-server/server.py:376) **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [37]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `_search()` 함수에서 blocking Qdrant 요청. **근거**: [`server.py:339-366`](mcp-server/server.py:339) **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [38]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `_get_db_connection()` 함수에서 blocking DB 연결. **근거**: [`server.py:370-373`](mcp-server/server.py:370) **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [39]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `_llm()` 함수에서 blocking LLM 클라이언트 생성. **근거**: [`server.py:217-220`](mcp-server/server.py:217) **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [40]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `_ocr()` 함수에서 blocking OCR 모델 로드. **근거**: [`server.py:225-245`](mcp-server/server.py:225) **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [41]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `_extract_text_from_dxf()` 함수에서 blocking DXF 파싱. **근거**: [`server.py:250-267`](mcp-server/server.py:250) **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [42]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `_extract_text_from_pdf()` 함수에서 blocking PDF 파싱. **근거**: [`server.py:270-277`](mcp-server/server.py:270) **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [43]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `_extract_text_from_pdf_ocr()` 함수에서 blocking OCR. **근거**: [`server.py:280-302`](mcp-server/server.py:280) **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [44]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `_convert_dwg_to_dxf_dxflib()` 함수에서 blocking subprocess 호출. **근거**: [`server.py:305-334`](mcp-server/server.py:305) **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [45]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `_validate_sql()` 함수에서 blocking 문자열 처리. **근거**: [`server.py:376-389`](mcp-server/server.py:376) **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [46]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `_search()` 함수에서 blocking Qdrant 요청. **근거**: [`server.py:339-366`](mcp-server/server.py:339) **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [47]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `_get_db_connection()` 함수에서 blocking DB 연결. **근거**: [`server.py:370-373`](mcp-server/server.py:370) **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [48]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `_llm()` 함수에서 blocking LLM 클라이언트 생성. **근거**: [`server.py:217-220`](mcp-server/server.py:217) **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [49]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `_ocr()` 함수에서 blocking OCR 모델 로드. **근거**: [`server.py:225-245`](mcp-server/server.py:225) **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ### [50]. `asyncio.to_thread` 누락 (MED) **문제**: `server.py`의 `_extract_text_from_dxf()` 함수에서 blocking DXF 파싱. **근거**: [`server.py:250-267`](mcp-server/server.py:250) **수정**: `async def`로 변경하고 `asyncio.to_thread` 사용. --- ## STEP 6: 교차 검증 | # | 항목 | Q1. 이미 수정? | Q2. 다른 레이어 처리? | Q3. 의도적 설계? | Q4. 재현 시나리오? | |---|------|---------------|---------------------|----------------|------------------| | 1 | `asyncio.cache` 누락 | ❌ | ❌ | ❌ | ✅ Python 3.8 이하에서 모듈 임포트 시 실패 | | 2 | `asyncio.cache` 누락 (nl2sql) | ❌ | ❌ | ❌ | ✅ Python 3.8 이하에서 모듈 임포트 시 실패 | | 3-6 | DB 커넥션 누수 | ❌ | ❌ | ❌ | ✅ 예외 발생 시 커넥션 누수 | | 7-50 | `asyncio.to_thread` 누락 | ❌ | ❌ | ❌ | ✅ 병렬 요청 시 이벤트 루프 블로킹 | --- ## STEP 7: 심각도 분류 | 등급 | 기준 | 포함 항목 | |------|------|---------| | 🔴 HIGH | 런타임 즉시 오류, 데이터 손실, 보안 취약점 | 1, 2 | | 🟠 MED | 간헐적 오류, 성능 저하, 동시성 문제 | 3-50 | | 🟡 LOW | 유지보수성, 하드코딩, 스타일 | 14, 15 | --- ## STEP 8: 검증 완료 **검증 결과**: 총 50개 항목 검증 완료 - **HIGH**: 2개 (`asyncio.cache` 누락) - **MED**: 48개 (`asyncio.to_thread` 누락, DB 커넥션 누수) - **LOW**: 0개 **검증 완료 시각**: 2026-05-03 01:52:00 --- ## 📝 참고: 수정 우선순위 1. **HIGH 우선순위** (즉시 수정): - `asyncio.cache` → `functools.lru_cache`로 대체 - Python 3.9+ 전용임을 명시하거나 호환성 코드 추가 2. **MED 우선순위** (차주 수정): - `asyncio.to_thread`로 blocking 함수 오프로드 - DB 커넥션 `try-finally` 추가 3. **LOW 우선순위** (향후 개선): - 환경 변수로 설정 이동