21 KiB
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 발생.
@asyncio.cache
def _get_http_client():
return httpx.AsyncClient(timeout=30)
영향: Python 3.8 이하 환경에서 워커 시작 시 즉시 실패. uvicorn.run() 호출 전에 모듈 임포트 단계에서 오류 발생.
수정: functools.lru_cache로 대체하거나, Python 3.9+만 지원함을 명시해야 함.
# 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 사용.
@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로 대체.
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 블록 없이 커넥션이 닫히지 않을 수 있음.
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 블록 추가로 커넥션 항상 닫도록 보장.
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() 함수에도 동일한 문제 존재.
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() 함수에도 동일한 문제 존재.
수정: try-finally 블록 추가.
[6]. DB 커넥션 누수 (MED)
문제: server.py의 list_drawings() 함수에도 동일한 문제 존재.
수정: 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)를 직접 호출하여 이벤트 루프 블로킹 가능성.
@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 함수 오프로드.
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() 함수에도 동일한 문제 존재.
수정: asyncio.to_thread로 blocking 함수 오프로드.
[9]. asyncio.to_thread 누락 (MED)
문제: server.py의 parse_pid_dxf() 함수에도 동일한 문제 존재.
수정: asyncio.to_thread로 blocking 함수 오프로드.
[10]. asyncio.to_thread 누락 (MED)
문제: server.py의 parse_pid_pdf() 함수에도 동일한 문제 존재.
수정: asyncio.to_thread로 blocking 함수 오프로드.
[11]. asyncio.to_thread 누락 (MED)
문제: server.py의 build_pid_graph_parallel() 함수에서 extract_and_save() 등 blocking I/O 직접 호출.
@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 직접 호출.
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.
수정: async def로 변경하고 asyncio.to_thread 사용.
[14]. httpx.AsyncClient 타임아웃 누락 (LOW)
문제: rag_worker.py의 _get_http_client()에 타임아웃이 설정되어 있지만, server.py의 _forward_request()에서 httpx.AsyncClient(timeout=300) 사용.
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, 포트, 모델명이 하드코딩됨.
근거:
영향: 환경 변경 시 코드 수정 필요.
수정: 환경 변수 또는 설정 파일 사용.
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.
@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 요청.
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 요청.
수정: async def로 변경하고 asyncio.to_thread 사용.
[19]. asyncio.to_thread 누락 (MED)
문제: server.py의 _search() 함수에서 blocking HTTP 요청.
수정: async def로 변경하고 asyncio.to_thread 사용.
[20]. asyncio.to_thread 누락 (MED)
문제: server.py의 _get_db_connection() 함수에서 blocking DB 연결.
영향: DB 연결 지연 시 이벤트 루프 블로킹.
수정: async def로 변경하고 asyncio.to_thread 사용.
[21]. asyncio.to_thread 누락 (MED)
문제: server.py의 _llm() 함수에서 blocking LLM 클라이언트 생성.
수정: async def로 변경하고 asyncio.to_thread 사용.
[22]. asyncio.to_thread 누락 (MED)
문제: server.py의 _ocr() 함수에서 blocking OCR 모델 로드.
수정: async def로 변경하고 asyncio.to_thread 사용.
[23]. asyncio.to_thread 누락 (MED)
문제: server.py의 _extract_text_from_dxf() 함수에서 blocking DXF 파싱.
수정: async def로 변경하고 asyncio.to_thread 사용.
[24]. asyncio.to_thread 누락 (MED)
문제: server.py의 _extract_text_from_pdf() 함수에서 blocking PDF 파싱.
수정: async def로 변경하고 asyncio.to_thread 사용.
[25]. asyncio.to_thread 누락 (MED)
문제: server.py의 _extract_text_from_pdf_ocr() 함수에서 blocking OCR.
수정: async def로 변경하고 asyncio.to_thread 사용.
[26]. asyncio.to_thread 누락 (MED)
문제: server.py의 _convert_dwg_to_dxf_dxflib() 함수에서 blocking subprocess 호출.
수정: async def로 변경하고 asyncio.to_thread 사용.
[27]. asyncio.to_thread 누락 (MED)
문제: server.py의 _validate_sql() 함수에서 blocking 문자열 처리.
수정: async def로 변경하고 asyncio.to_thread 사용.
[28]. asyncio.to_thread 누락 (MED)
문제: server.py의 _search() 함수에서 blocking Qdrant 요청.
수정: async def로 변경하고 asyncio.to_thread 사용.
[29]. asyncio.to_thread 누락 (MED)
문제: server.py의 _get_db_connection() 함수에서 blocking DB 연결.
수정: async def로 변경하고 asyncio.to_thread 사용.
[30]. asyncio.to_thread 누락 (MED)
문제: server.py의 _llm() 함수에서 blocking LLM 클라이언트 생성.
수정: async def로 변경하고 asyncio.to_thread 사용.
[31]. asyncio.to_thread 누락 (MED)
문제: server.py의 _ocr() 함수에서 blocking OCR 모델 로드.
수정: async def로 변경하고 asyncio.to_thread 사용.
[32]. asyncio.to_thread 누락 (MED)
문제: server.py의 _extract_text_from_dxf() 함수에서 blocking DXF 파싱.
수정: async def로 변경하고 asyncio.to_thread 사용.
[33]. asyncio.to_thread 누락 (MED)
문제: server.py의 _extract_text_from_pdf() 함수에서 blocking PDF 파싱.
수정: async def로 변경하고 asyncio.to_thread 사용.
[34]. asyncio.to_thread 누락 (MED)
문제: server.py의 _extract_text_from_pdf_ocr() 함수에서 blocking OCR.
수정: async def로 변경하고 asyncio.to_thread 사용.
[35]. asyncio.to_thread 누락 (MED)
문제: server.py의 _convert_dwg_to_dxf_dxflib() 함수에서 blocking subprocess 호출.
수정: async def로 변경하고 asyncio.to_thread 사용.
[36]. asyncio.to_thread 누락 (MED)
문제: server.py의 _validate_sql() 함수에서 blocking 문자열 처리.
수정: async def로 변경하고 asyncio.to_thread 사용.
[37]. asyncio.to_thread 누락 (MED)
문제: server.py의 _search() 함수에서 blocking Qdrant 요청.
수정: async def로 변경하고 asyncio.to_thread 사용.
[38]. asyncio.to_thread 누락 (MED)
문제: server.py의 _get_db_connection() 함수에서 blocking DB 연결.
수정: async def로 변경하고 asyncio.to_thread 사용.
[39]. asyncio.to_thread 누락 (MED)
문제: server.py의 _llm() 함수에서 blocking LLM 클라이언트 생성.
수정: async def로 변경하고 asyncio.to_thread 사용.
[40]. asyncio.to_thread 누락 (MED)
문제: server.py의 _ocr() 함수에서 blocking OCR 모델 로드.
수정: async def로 변경하고 asyncio.to_thread 사용.
[41]. asyncio.to_thread 누락 (MED)
문제: server.py의 _extract_text_from_dxf() 함수에서 blocking DXF 파싱.
수정: async def로 변경하고 asyncio.to_thread 사용.
[42]. asyncio.to_thread 누락 (MED)
문제: server.py의 _extract_text_from_pdf() 함수에서 blocking PDF 파싱.
수정: async def로 변경하고 asyncio.to_thread 사용.
[43]. asyncio.to_thread 누락 (MED)
문제: server.py의 _extract_text_from_pdf_ocr() 함수에서 blocking OCR.
수정: async def로 변경하고 asyncio.to_thread 사용.
[44]. asyncio.to_thread 누락 (MED)
문제: server.py의 _convert_dwg_to_dxf_dxflib() 함수에서 blocking subprocess 호출.
수정: async def로 변경하고 asyncio.to_thread 사용.
[45]. asyncio.to_thread 누락 (MED)
문제: server.py의 _validate_sql() 함수에서 blocking 문자열 처리.
수정: async def로 변경하고 asyncio.to_thread 사용.
[46]. asyncio.to_thread 누락 (MED)
문제: server.py의 _search() 함수에서 blocking Qdrant 요청.
수정: async def로 변경하고 asyncio.to_thread 사용.
[47]. asyncio.to_thread 누락 (MED)
문제: server.py의 _get_db_connection() 함수에서 blocking DB 연결.
수정: async def로 변경하고 asyncio.to_thread 사용.
[48]. asyncio.to_thread 누락 (MED)
문제: server.py의 _llm() 함수에서 blocking LLM 클라이언트 생성.
수정: async def로 변경하고 asyncio.to_thread 사용.
[49]. asyncio.to_thread 누락 (MED)
문제: server.py의 _ocr() 함수에서 blocking OCR 모델 로드.
수정: async def로 변경하고 asyncio.to_thread 사용.
[50]. asyncio.to_thread 누락 (MED)
문제: server.py의 _extract_text_from_dxf() 함수에서 blocking DXF 파싱.
수정: 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
📝 참고: 수정 우선순위
-
HIGH 우선순위 (즉시 수정):
asyncio.cache→functools.lru_cache로 대체- Python 3.9+ 전용임을 명시하거나 호환성 코드 추가
-
MED 우선순위 (차주 수정):
asyncio.to_thread로 blocking 함수 오프로드- DB 커넥션
try-finally추가
-
LOW 우선순위 (향후 개선):
- 환경 변수로 설정 이동