Honeywell HC900을 Modbus TCP로 직접 폴링 → gRPC → C# 크롤러 → PostgreSQL. 기존 Experion OPC UA 데이터 경로를 HC900 직접 통신으로 대체. - industrial-comm/cpp: C++ Modbus 게이트웨이 (gRPC 서버) - src: C# .NET 8 ASP.NET Core 크롤러 + 웹 UI (3-Layer) - mcp-server: Python FastMCP (RAG/NL2SQL/P&ID) - 다중 컨트롤러(N-Controller) 지원 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
728 lines
21 KiB
Markdown
728 lines
21 KiB
Markdown
# 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 우선순위** (향후 개선):
|
|
- 환경 변수로 설정 이동
|