9.2 KiB
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
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
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
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, pid_worker.py:400-401
문제: _build_pid_graph_parallel()에서 PidTopologyBuilder.build_graph()를 2번 호출
# 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— import:networkx,shapely,json,typing— 공간 인덱스 라이브러리 없음extractor.py:176-183—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() 중복 호출을 단일 호출로 통합할 것.