# pid-그래프병목진단-수정방안.md 진단 검증 보고서

> 검증일: 2026-05-04  
> 검증 기준: diagnosis-checklist.md (8단계)  
> 검증 대상: pid-그래프병목진단-수정방안.md  

---

## 검증 요약

| 진단 항목 | 원 심각도 | 재검증 심각도 | 결론 |
|-----------|-----------|---------------|------|
| 병목 1: 배관-설비 연결 O(n²) | HIGH | HIGH | ✅ **정당함** |
| 병목 2: 태그-설비 매핑 O(n²) | MED | MED | ✅ **정당함** |
| 병목 3: DXF 엔티티 순회 | LOW | LOW | ✅ **정당함** |
| — 추가 발견 — | — | MED | ⚠️ **build_graph() 중복 호출** |

**전체 결론: 진단 문서의 3개 항목 모두 정당함. 추가 1개 항목 발견.**

---

## STEP 4 — 호출 계층 지도

```
pid_worker._build_pid_graph_parallel()  (async, pid_worker.py:318)
  │
  ├─ Phase 1: PidGeometricExtractor.extract_and_save()
  │   └─ for entity in self.msp:          (28,819개 순회)
  │       └─ get_bbox(entity)             — BBox 계산, O(n)
  │
  ├─ Phase 2: PidTopologyBuilder.build_graph()  ← 1차 호출
  │   ├─ 노드 추가 (geo_data)
  │   ├─ _find_nearest_equipment()        ← O(TEXT × EQUIPMENT) ≈ 1,280만 회
  │   │   └─ tag_bbox.distance(eq_bbox)   — shapely 거리 계산
  │   └─ 배관-설비 연결                   ← O(LINE × EQUIPMENT) ≈ 7,110만 회
  │       ├─ line_geom.intersects(eq_bbox)  — shapely 교차 계산
  │       └─ line_geom.distance(eq_bbox)    — shapely 거리 계산
  │
  ├─ Phase 3: IntelligentMapper (병렬 LLM)
  │
  └─ Phase 4: PidTopologyBuilder.build_graph()  ← 2차 호출 (동일 O(n²) 반복)
      └─ 동일한 배관-설비 연결 + 태그-설비 매핑 재실행
```

---

## STEP 5~6 — 진단 항목逐个 검증

---

### 1. 병목 1: Phase 2 — `build_graph()` 내 배관-설비 연결 로직 (HIGH) ✅ 정당

**진단 내용**: LINE/LWPOLYLINE(약 21,733개)과 설비(약 3,272개)를 전체 조합으로 비교

**코드 확인**: [`topology.py:82-99`](mcp-server/pipeline/topology.py:82)

```python
lines = [n for n, d in self.G.nodes(data=True) if d['type'] in ['LINE', 'LWPOLYLINE']]
for line_id in lines:                          # 21,733개
    ...
    for eq_id in equipments:                   # 3,272개
        eq_bbox = self.G.nodes[eq_id]['bbox']
        if line_geom.intersects(eq_bbox):      # shapely 교차 계산 (고비용)
            connected_nodes.append(eq_id)
        elif line_geom.distance(eq_bbox) < self.config['dist_threshold']:
            connected_nodes.append(eq_id)
```

**교차 검증 4문항:**

| 질문 | 결과 | 판단 |
|------|------|------|
| Q1. 이미 수정된 문제인가? | 코드 확인: 여전히 중첩 for문, 공간 인덱스 없음 | ❌ 아님 → 유지 |
| Q2. 다른 레이어에서 처리되고 있는가? | 상위 호출자인 `_build_pid_graph_parallel()`도 필터링 없음 | ❌ 아님 → 유지 |
| Q3. 의도적 설계인가? | 문서/주석에 "의도적 O(n²)" 설명 없음 | ❌ 아님 → 유지 |
| Q4. 실제 장애 시나리오가 있는가? | 21,733 × 3,272 = 7,110만 회 shapely 연산 → 10분 이상 타임아웃 | ✅ 있음 → 유지 |

**재검증 심각도: 🔴 HIGH (유지)**

---

### 2. 병목 2: Phase 2 — `_find_nearest_equipment()` 태그-설비 매핑 (MED) ✅ 정당

**진단 내용**: TEXT/MTEXT(약 3,925개)와 설비(약 3,272개)를 전체 비교

**코드 확인**: [`topology.py:143-148`](mcp-server/pipeline/topology.py:143)

```python
for eq_id in equipment_ids:                    # 3,272개
    eq_bbox = self.G.nodes[eq_id]['bbox']
    dist = tag_bbox.distance(eq_bbox)          # shapely 거리 계산
    if dist > self.config['tag_threshold']:
        continue
```

**교차 검증 4문항:**

| 질문 | 결과 | 판단 |
|------|------|------|
| Q1. 이미 수정된 문제인가? | 코드 확인: 여전히 전체 순회, 빠른 필터 없음 | ❌ 아님 → 유지 |
| Q2. 다른 레이어에서 처리되고 있는가? | `_find_nearest_equipment()`가 독립 함수 — 상위에서 필터링 없음 | ❌ 아님 → 유지 |
| Q3. 의도적 설계인가? | 주석에 "위상 기반 가중치 매핑"이라고 하지만 O(n²)은 의도적이지 않음 | ❌ 아님 → 유지 |
| Q4. 실제 장애 시나리오가 있는가? | 3,925 × 3,272 = 1,280만 회 — 수초~수십초 지연 발생 | ✅ 있음 → 유지 |

**재검증 심각도: 🟠 MED (유지)**

---

### 3. 병목 3: Phase 1 — DXF 엔티티 순회 추출 (LOW) ✅ 정당

**진단 내용**: 28,819개 엔티티를 하나씩 순회하며 BBox 계산

**코드 확인**: [`extractor.py:124-162`](mcp-server/pipeline/extractor.py:124)

```python
for entity in self.msp:                        # 28,819개
    try:
        bbox_obj = self.get_bbox(entity)       # 각 엔티티당 BBox 계산
        if not bbox_obj:
            continue
        ...
```

**교차 검증 4문항:**

| 질문 | 결과 | 판단 |
|------|------|------|
| Q1. 이미 수정된 문제인가? | N/A — O(n) 순회는 DXF 파싱의 본질 | N/A |
| Q2. 다른 레이어에서 처리되고 있는가? | N/A | N/A |
| Q3. 의도적 설계인가? | DXF 파일은 순차 읽기 구조 — 필수적 | ✅ 맞음 → LOW 유지 |
| Q4. 실제 장애 시나리오가 있는가? | ~1.4초 — 허용 범위 | ❌ 없음 → LOW 유지 |

**재검증 심각도: 🟡 LOW (유지)**

---

## ⚠️ 추가 발견 사항

### 4. `build_graph()` 중복 호출 (MED) — 진단 문서에 누락

**위치**: [`pid_worker.py:350-351`](mcp-server/worker/pid_worker.py:350), [`pid_worker.py:400-401`](mcp-server/worker/pid_worker.py:400)

**문제**: `_build_pid_graph_parallel()`에서 `PidTopologyBuilder.build_graph()`를 **2번 호출**

```python
# Phase 2: 1차 위상 빌더 (Mapper용 그래프)
builder = PidTopologyBuilder(geo_data)
builder.build_graph()                          # ← 1차: O(n²) 전체 실행

# ... Phase 3: LLM 매핑 ...

# Phase 4: 최종 위상 모델링 + 저장
final_builder = PidTopologyBuilder(geo_data, all_extracted_tags=all_mapped_tags)
final_builder.build_graph()                    # ← 2차: 동일한 O(n²) 전체 재실행
```

**근거**:
- 1차 호출: 배관-설비 연결 (7,110만 회) + 태그-설비 매핑 (1,280만 회)
- 2차 호출: 동일한 연산을 `all_mapped_tags`를 포함해서 다시 실행
- 총 연산량: (7,110만 + 1,280만) × 2 = **약 16,780만 회**

**영향**: Phase 2 + Phase 4가 합쳐져 병목 시간이 약 2배 증가

**수정 방향**:
- Phase 4에서 `build_graph()` 대신 1차 그래프를 재사용하고, `all_mapped_tags`만 추가 연결
- 또는 Phase 2에서 `all_mapped_tags`를 미리 받아서 1회 호출로 통합

**교차 검증 4문항:**

| 질문 | 결과 | 판단 |
|------|------|------|
| Q1. 이미 수정된 문제인가? | 코드 확인: 2번 호출 그대로 | ❌ 아님 → 추가 |
| Q2. 다른 레이어에서 처리되고 있는가? | N/A — 같은 함수 내에서 발생 | ❌ 아님 → 추가 |
| Q3. 의도적 설계인가? | "1차 위상 빌더 (Mapper용)" / "최종 위상 모델링" 주석이 있지만, 동일한 O(n²)을 2번 하는 것은 설계 실수 | ❌ 아님 → 추가 |
| Q4. 실제 장애 시나리오가 있는가? | build_graph()가 10분 걸리면 20분으로 증가 | ✅ 있음 → 추가 |

**재검증 심각도: 🟠 MED**

---

## 근본 원인 검증

**진단 문서 주장**: "공간 인덱스(R-tree, Quadtree) 미사용으로 모든 노드 쌍을 O(n²)으로 비교"

**코드 검증 결과**: ✅ **정확함**

- [`topology.py`](mcp-server/pipeline/topology.py:1) — import: `networkx`, `shapely`, `json`, `typing` — 공간 인덱스 라이브러리 없음
- [`extractor.py:176-183`](mcp-server/pipeline/extractor.py:176) — `is_near()` 함수도 매번 `shapely.box()` 생성 후 거리 계산
- 전체 프로젝트에서 `rtree`, `strtree`, `quadtree` 관련 import 없음

---

## 수정 계획 검증

| Step | 진단 문서 제안 | 검증 결과 |
|------|----------------|-----------|
| Step 1: BBox 빠른 필터링 | shapely 없이 min/max 좌표로 사전 필터 | ✅ 타당 — O(1) 비교로 90% 이상 후보 제거 가능 |
| Step 2: 그리드 기반 공간 인덱스 | SpatialGrid 클래스 추가, 셀 크기 = dist_threshold | ✅ 타당 — O(n log n)으로 감소 |
| Step 3: pid_worker.py 통합 | E2E 테스트 | ✅ 타당 — 하지만 #4(중복 호출)도 함께 수정 필요 |

**추가 제안**: Step 1-2 실행 시 `pid_worker.py`의 Phase 2/4 중복 호출도 함께 수정할 것.

---

## 최종 판단

| 항목 | 진단 문서 | 재검증 | 일치 |
|------|-----------|--------|------|
| 병목 1: 배관-설비 O(n²) | HIGH | HIGH | ✅ |
| 병목 2: 태그-설비 O(n²) | MED | MED | ✅ |
| 병목 3: DXF 순회 | LOW | LOW | ✅ |
| 근본 원인: 공간 인덱스 부재 | O(n²) | O(n²) | ✅ |
| 수정 계획: 3단계 점진적 | BBox→Grid→E2E | BBox→Grid→E2E | ✅ |
| **누락: build_graph() 2중 호출** | — | MED | ⚠️ |

**진단 문서의 정확도: 5/5 항목 정확, 1개 추가 발견**

**수정 계획에 반영할 사항**: Step 3 실행 시 `pid_worker.py:350-401`의 `build_graph()` 중복 호출을 단일 호출로 통합할 것.
