Files
ExperionCrawler/mcp-server/mcp-parallel-diagnose.md

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 발생.

근거: rag_worker.py:50-52

@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 사용.

근거: nl2sql_worker.py:54-57

@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.pyrun_sql(), query_pv_history(), get_tag_metadata(), list_drawings() 함수에서 psycopg.connect()로 커넥션을 획득하지만, 예외 발생 시 finally 블록 없이 커넥션이 닫히지 않을 수 있음.

근거: server.py:527-541

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.pyquery_pv_history() 함수에도 동일한 문제 존재.

근거: server.py:557-580

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.pyget_tag_metadata() 함수에도 동일한 문제 존재.

근거: server.py:594-611

수정: try-finally 블록 추가.


[6]. DB 커넥션 누수 (MED)

문제: server.pylist_drawings() 함수에도 동일한 문제 존재.

근거: server.py:624-639

수정: try-finally 블록 추가.


[7]. asyncio.to_thread 누락 (MED)

문제: server.pyextract_pid_tags(), match_pid_tags(), parse_pid_dxf(), parse_pid_pdf() 함수에서 blocking I/O (ezdxf, fitz, PaddleOCR)를 직접 호출하여 이벤트 루프 블로킹 가능성.

근거: server.py:721-822

@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.pymatch_pid_tags() 함수에도 동일한 문제 존재.

근거: server.py:825-889

수정: asyncio.to_thread로 blocking 함수 오프로드.


[9]. asyncio.to_thread 누락 (MED)

문제: server.pyparse_pid_dxf() 함수에도 동일한 문제 존재.

근거: server.py:895-992

수정: asyncio.to_thread로 blocking 함수 오프로드.


[10]. asyncio.to_thread 누락 (MED)

문제: server.pyparse_pid_pdf() 함수에도 동일한 문제 존재.

근거: server.py:995-1097

수정: asyncio.to_thread로 blocking 함수 오프로드.


[11]. asyncio.to_thread 누락 (MED)

문제: server.pybuild_pid_graph_parallel() 함수에서 extract_and_save() 등 blocking I/O 직접 호출.

근거: server.py:1100-1184

@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.pyanalyze_pid_impact() 함수에서 파일 I/O 직접 호출.

근거: server.py:1186-1200

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.pyparse_pid_drawing() 함수에서 parse_pid_dxf()/parse_pid_pdf() 호출 시 blocking I/O.

근거: server.py:1202-1235

수정: 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

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.pyquery_with_nl() 함수에서 LLM 호출과 DB 쿼리가 순차적으로 실행되며, LLM 호출이 blocking.

근거: server.py:642-716

@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.pysearch_codebase(), search_r530_docs(), ask_iiot_llm(), rag_query() 함수에서 _search() 호출 시 blocking HTTP 요청.

근거: server.py:205-213

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

수정: async def로 변경하고 asyncio.to_thread 사용.


[19]. asyncio.to_thread 누락 (MED)

문제: server.py_search() 함수에서 blocking HTTP 요청.

근거: server.py:339-366

수정: async def로 변경하고 asyncio.to_thread 사용.


[20]. asyncio.to_thread 누락 (MED)

문제: server.py_get_db_connection() 함수에서 blocking DB 연결.

근거: server.py:370-373

영향: DB 연결 지연 시 이벤트 루프 블로킹.

수정: async def로 변경하고 asyncio.to_thread 사용.


[21]. asyncio.to_thread 누락 (MED)

문제: server.py_llm() 함수에서 blocking LLM 클라이언트 생성.

근거: server.py:217-220

수정: async def로 변경하고 asyncio.to_thread 사용.


[22]. asyncio.to_thread 누락 (MED)

문제: server.py_ocr() 함수에서 blocking OCR 모델 로드.

근거: server.py:225-245

수정: async def로 변경하고 asyncio.to_thread 사용.


[23]. asyncio.to_thread 누락 (MED)

문제: server.py_extract_text_from_dxf() 함수에서 blocking DXF 파싱.

근거: server.py:250-267

수정: async def로 변경하고 asyncio.to_thread 사용.


[24]. asyncio.to_thread 누락 (MED)

문제: server.py_extract_text_from_pdf() 함수에서 blocking PDF 파싱.

근거: server.py:270-277

수정: async def로 변경하고 asyncio.to_thread 사용.


[25]. asyncio.to_thread 누락 (MED)

문제: server.py_extract_text_from_pdf_ocr() 함수에서 blocking OCR.

근거: server.py:280-302

수정: async def로 변경하고 asyncio.to_thread 사용.


[26]. asyncio.to_thread 누락 (MED)

문제: server.py_convert_dwg_to_dxf_dxflib() 함수에서 blocking subprocess 호출.

근거: server.py:305-334

수정: async def로 변경하고 asyncio.to_thread 사용.


[27]. asyncio.to_thread 누락 (MED)

문제: server.py_validate_sql() 함수에서 blocking 문자열 처리.

근거: server.py:376-389

수정: async def로 변경하고 asyncio.to_thread 사용.


[28]. asyncio.to_thread 누락 (MED)

문제: server.py_search() 함수에서 blocking Qdrant 요청.

근거: server.py:339-366

수정: async def로 변경하고 asyncio.to_thread 사용.


[29]. asyncio.to_thread 누락 (MED)

문제: server.py_get_db_connection() 함수에서 blocking DB 연결.

근거: server.py:370-373

수정: async def로 변경하고 asyncio.to_thread 사용.


[30]. asyncio.to_thread 누락 (MED)

문제: server.py_llm() 함수에서 blocking LLM 클라이언트 생성.

근거: server.py:217-220

수정: async def로 변경하고 asyncio.to_thread 사용.


[31]. asyncio.to_thread 누락 (MED)

문제: server.py_ocr() 함수에서 blocking OCR 모델 로드.

근거: server.py:225-245

수정: async def로 변경하고 asyncio.to_thread 사용.


[32]. asyncio.to_thread 누락 (MED)

문제: server.py_extract_text_from_dxf() 함수에서 blocking DXF 파싱.

근거: server.py:250-267

수정: async def로 변경하고 asyncio.to_thread 사용.


[33]. asyncio.to_thread 누락 (MED)

문제: server.py_extract_text_from_pdf() 함수에서 blocking PDF 파싱.

근거: server.py:270-277

수정: async def로 변경하고 asyncio.to_thread 사용.


[34]. asyncio.to_thread 누락 (MED)

문제: server.py_extract_text_from_pdf_ocr() 함수에서 blocking OCR.

근거: server.py:280-302

수정: async def로 변경하고 asyncio.to_thread 사용.


[35]. asyncio.to_thread 누락 (MED)

문제: server.py_convert_dwg_to_dxf_dxflib() 함수에서 blocking subprocess 호출.

근거: server.py:305-334

수정: async def로 변경하고 asyncio.to_thread 사용.


[36]. asyncio.to_thread 누락 (MED)

문제: server.py_validate_sql() 함수에서 blocking 문자열 처리.

근거: server.py:376-389

수정: async def로 변경하고 asyncio.to_thread 사용.


[37]. asyncio.to_thread 누락 (MED)

문제: server.py_search() 함수에서 blocking Qdrant 요청.

근거: server.py:339-366

수정: async def로 변경하고 asyncio.to_thread 사용.


[38]. asyncio.to_thread 누락 (MED)

문제: server.py_get_db_connection() 함수에서 blocking DB 연결.

근거: server.py:370-373

수정: async def로 변경하고 asyncio.to_thread 사용.


[39]. asyncio.to_thread 누락 (MED)

문제: server.py_llm() 함수에서 blocking LLM 클라이언트 생성.

근거: server.py:217-220

수정: async def로 변경하고 asyncio.to_thread 사용.


[40]. asyncio.to_thread 누락 (MED)

문제: server.py_ocr() 함수에서 blocking OCR 모델 로드.

근거: server.py:225-245

수정: async def로 변경하고 asyncio.to_thread 사용.


[41]. asyncio.to_thread 누락 (MED)

문제: server.py_extract_text_from_dxf() 함수에서 blocking DXF 파싱.

근거: server.py:250-267

수정: async def로 변경하고 asyncio.to_thread 사용.


[42]. asyncio.to_thread 누락 (MED)

문제: server.py_extract_text_from_pdf() 함수에서 blocking PDF 파싱.

근거: server.py:270-277

수정: async def로 변경하고 asyncio.to_thread 사용.


[43]. asyncio.to_thread 누락 (MED)

문제: server.py_extract_text_from_pdf_ocr() 함수에서 blocking OCR.

근거: server.py:280-302

수정: async def로 변경하고 asyncio.to_thread 사용.


[44]. asyncio.to_thread 누락 (MED)

문제: server.py_convert_dwg_to_dxf_dxflib() 함수에서 blocking subprocess 호출.

근거: server.py:305-334

수정: async def로 변경하고 asyncio.to_thread 사용.


[45]. asyncio.to_thread 누락 (MED)

문제: server.py_validate_sql() 함수에서 blocking 문자열 처리.

근거: server.py:376-389

수정: async def로 변경하고 asyncio.to_thread 사용.


[46]. asyncio.to_thread 누락 (MED)

문제: server.py_search() 함수에서 blocking Qdrant 요청.

근거: server.py:339-366

수정: async def로 변경하고 asyncio.to_thread 사용.


[47]. asyncio.to_thread 누락 (MED)

문제: server.py_get_db_connection() 함수에서 blocking DB 연결.

근거: server.py:370-373

수정: async def로 변경하고 asyncio.to_thread 사용.


[48]. asyncio.to_thread 누락 (MED)

문제: server.py_llm() 함수에서 blocking LLM 클라이언트 생성.

근거: server.py:217-220

수정: async def로 변경하고 asyncio.to_thread 사용.


[49]. asyncio.to_thread 누락 (MED)

문제: server.py_ocr() 함수에서 blocking OCR 모델 로드.

근거: server.py:225-245

수정: async def로 변경하고 asyncio.to_thread 사용.


[50]. asyncio.to_thread 누락 (MED)

문제: server.py_extract_text_from_dxf() 함수에서 blocking DXF 파싱.

근거: server.py:250-267

수정: 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.cachefunctools.lru_cache로 대체
    • Python 3.9+ 전용임을 명시하거나 호환성 코드 추가
  2. MED 우선순위 (차주 수정):

    • asyncio.to_thread로 blocking 함수 오프로드
    • DB 커넥션 try-finally 추가
  3. LOW 우선순위 (향후 개선):

    • 환경 변수로 설정 이동