아키텍처 복잡도 시각화 — tpcall 호출그래프 (모듈뷰 + 서비스뷰)
IR·데모용으로 "복잡하게 얽힌 시스템"을 눈에 보이게 만든다. 지금까지 복잡도는 백엔드에 실재했지만 그리드 화면에는 드러나지 않았다. - tools/gen_callgraph.py: svc/*.pgc 의 tpcall 관계를 추출해 호출그래프를 뽑고, force-directed 레이아웃을 사전계산(외부 의존성 0, 시드 고정)해 index.html 에 인라인 주입(게이트웨이는 index.html 만 서빙 → 외부 .js 로드 불가). 552 노드 · 494 엣지 · 11 모듈 · 41 모듈간 호출경로 · 최대 체인 깊이 6단. - 포털 신규 화면 "아키텍처·복잡도" (AR0001, 최상단 nav): · 모듈뷰: 11개 도메인을 링으로 배치, 방향 화살표 + 호출수 비례 두께 · 서비스뷰: 552개 서비스 hairball, 모듈 색상 + 호출차수 비례 크기, 팬/줌/호버 · 대표 심층 업무체인 5개 칩(ACQUIRE 6단 등) 클릭 시 경로 하이라이트 · KPI: 온라인 1,463 / 배치 506 / 체인연결 552 / 엣지 494 / 최대깊이 6단 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
074d244bba
commit
5b572989e3
3 changed files with 329 additions and 0 deletions
178
tools/gen_callgraph.py
Normal file
178
tools/gen_callgraph.py
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
gen_callgraph.py — svc/*.pgc 의 tpcall 관계를 추출해 아키텍처 복잡도 시각화용
|
||||
데이터(app/ui/callgraph.js)를 만든다.
|
||||
|
||||
산출물:
|
||||
CG_MODULES : 모듈 노드 11개 (id, 한글명, 서비스수, 교차호출 in/out)
|
||||
CG_MODEDGES : 모듈간 호출 엣지 (from,to,count) ← 상위 얽힘 뷰
|
||||
CG_NODES : 서비스 노드 552개 (id, module, x, y, deg) ← 전체 hairball
|
||||
CG_EDGES : 서비스 호출 엣지 494개 (from,to)
|
||||
CG_CHAINS : 대표 심층 체인 (ACQUIRE 6단 등) ← 하이라이트
|
||||
|
||||
레이아웃은 순수 파이썬 force-directed 로 여기서 미리 계산한다(외부 의존성 0).
|
||||
결정적 재현을 위해 시드 고정.
|
||||
"""
|
||||
import glob, re, os, json, math, collections, random
|
||||
|
||||
random.seed(42)
|
||||
SRC = os.path.join(os.path.dirname(__file__), "..", "app", "src")
|
||||
OUT = os.path.join(os.path.dirname(__file__), "..", "app", "ui", "callgraph.js")
|
||||
|
||||
MODNM = {
|
||||
"ac": "매입", "au": "승인/한도", "st": "정산/수수료", "py": "지급",
|
||||
"lg": "원장", "cl": "마감", "rc": "대사", "vl": "정합성검증",
|
||||
"mm": "마스터", "mg": "전문게이트웨이", "cm": "공통",
|
||||
}
|
||||
|
||||
# ── 1. 추출 ────────────────────────────────────────────────────────────────
|
||||
node_mod, edges = {}, []
|
||||
svc_count = collections.Counter()
|
||||
for f in sorted(glob.glob(os.path.join(SRC, "*/svc/*.pgc"))):
|
||||
mod = os.path.normpath(f).split(os.sep)[-3]
|
||||
caller = os.path.basename(f)[:-4]
|
||||
node_mod[caller] = mod
|
||||
svc_count[mod] += 1
|
||||
txt = open(f, encoding="utf-8", errors="ignore").read()
|
||||
for callee in re.findall(r'tpcall\("([A-Z_0-9]+)"', txt):
|
||||
if callee != caller:
|
||||
edges.append((caller, callee))
|
||||
|
||||
# 호출에 관여하는 노드만 그래프에 (552개)
|
||||
active = set(a for a, _ in edges) | set(b for _, b in edges)
|
||||
deg = collections.Counter()
|
||||
for a, b in edges:
|
||||
deg[a] += 1
|
||||
deg[b] += 1
|
||||
|
||||
# ── 2. 모듈 그래프 ──────────────────────────────────────────────────────────
|
||||
modedge = collections.Counter()
|
||||
for a, b in edges:
|
||||
ma, mb = node_mod.get(a, "cm"), node_mod.get(b, "cm")
|
||||
if ma != mb:
|
||||
modedge[(ma, mb)] += 1
|
||||
mod_out = collections.Counter(); mod_in = collections.Counter()
|
||||
for (a, b), c in modedge.items():
|
||||
mod_out[a] += c; mod_in[b] += c
|
||||
|
||||
# ── 3. 대표 심층 체인 (BFS 로 가장 깊은 경로 몇 개) ──────────────────────────
|
||||
adj = collections.defaultdict(list)
|
||||
for a, b in edges:
|
||||
if b not in adj[a]:
|
||||
adj[a].append(b)
|
||||
|
||||
def deepest_from(start, maxd=8):
|
||||
best = [start]
|
||||
stack = [(start, [start], {start})]
|
||||
while stack:
|
||||
n, path, seen = stack.pop()
|
||||
if len(path) > len(best):
|
||||
best = path
|
||||
if len(path) >= maxd:
|
||||
continue
|
||||
for m in adj.get(n, []):
|
||||
if m not in seen:
|
||||
stack.append((m, path + [m], seen | {m}))
|
||||
return best
|
||||
|
||||
chains = []
|
||||
for root in ["ACQUIRE", "ACQ_SETTLELINK", "MG_RECV", "CL_DAILY", "PAY_APPROVE"]:
|
||||
if root in active:
|
||||
p = deepest_from(root)
|
||||
if len(p) >= 3:
|
||||
chains.append(p)
|
||||
|
||||
# ── 4. force-directed 레이아웃 (모듈별 초기 클러스터 → 스프링) ────────────────
|
||||
nodes = sorted(active)
|
||||
idx = {n: i for i, n in enumerate(nodes)}
|
||||
N = len(nodes)
|
||||
# 모듈을 원주에 배치해 같은 모듈끼리 뭉치게 초기화
|
||||
mods = sorted(MODNM)
|
||||
mang = {m: 2 * math.pi * i / len(mods) for i, m in enumerate(mods)}
|
||||
X = [0.0] * N; Y = [0.0] * N
|
||||
for n in nodes:
|
||||
m = node_mod.get(n, "cm")
|
||||
a = mang[m] + random.uniform(-0.35, 0.35)
|
||||
r = 340 + random.uniform(-60, 60)
|
||||
X[idx[n]] = math.cos(a) * r
|
||||
Y[idx[n]] = math.sin(a) * r
|
||||
|
||||
E = [(idx[a], idx[b]) for a, b in edges if a in idx and b in idx]
|
||||
K = 78.0 # 이상 스프링 길이
|
||||
REP = 3400.0 # 반발 상수
|
||||
GRAV = 0.020 # 중심 중력 (박스 아티팩트 억제 — 원형 블롭 유지)
|
||||
for it in range(340):
|
||||
fx = [0.0] * N; fy = [0.0] * N
|
||||
# 반발 (O(n^2), 552노드 = 15만/iter, 340회 ≈ 5천만 — 수초)
|
||||
for i in range(N):
|
||||
xi, yi = X[i], Y[i]
|
||||
for j in range(i + 1, N):
|
||||
dx = xi - X[j]; dy = yi - Y[j]
|
||||
d2 = dx * dx + dy * dy + 0.01
|
||||
f = REP / d2
|
||||
fdx = dx * f; fdy = dy * f
|
||||
fx[i] += fdx; fy[i] += fdy
|
||||
fx[j] -= fdx; fy[j] -= fdy
|
||||
# 인력 (엣지 스프링)
|
||||
for a, b in E:
|
||||
dx = X[a] - X[b]; dy = Y[a] - Y[b]
|
||||
d = math.sqrt(dx * dx + dy * dy) + 0.01
|
||||
f = (d - K) * 0.05
|
||||
fdx = dx / d * f; fdy = dy / d * f
|
||||
fx[a] -= fdx; fy[a] -= fdy
|
||||
fx[b] += fdx; fy[b] += fdy
|
||||
# 중심 중력 (반경 비례로 당겨 원형 유지)
|
||||
cool = 1.0 - it / 400.0
|
||||
for i in range(N):
|
||||
fx[i] -= X[i] * GRAV
|
||||
fy[i] -= Y[i] * GRAV
|
||||
X[i] += max(-22, min(22, fx[i])) * cool
|
||||
Y[i] += max(-22, min(22, fy[i])) * cool
|
||||
|
||||
# 정규화 → 종횡비 보존(단일 스케일), 중앙 정렬 → 0..1000 박스에 원형 블롭 유지
|
||||
xs = [X[i] for i in range(N)]; ys = [Y[i] for i in range(N)]
|
||||
minx, maxx, miny, maxy = min(xs), max(xs), min(ys), max(ys)
|
||||
span = max(maxx - minx, maxy - miny) or 1
|
||||
cx = (minx + maxx) / 2; cy = (miny + maxy) / 2
|
||||
def nx(v): return round((v - cx) / span * 900 + 500, 1)
|
||||
def ny(v): return round((v - cy) / span * 900 + 500, 1)
|
||||
|
||||
cg_nodes = [{"id": n, "m": node_mod.get(n, "cm"),
|
||||
"x": nx(X[idx[n]]), "y": ny(Y[idx[n]]),
|
||||
"d": deg[n]} for n in nodes]
|
||||
cg_edges = [[a, b] for a, b in edges if a in idx and b in idx]
|
||||
cg_modules = [{"id": m, "nm": MODNM[m], "svc": svc_count[m],
|
||||
"in": mod_in[m], "out": mod_out[m]} for m in mods]
|
||||
cg_modedges = [{"f": a, "t": b, "c": c} for (a, b), c in modedge.items()]
|
||||
|
||||
data = {
|
||||
"MODULES": cg_modules,
|
||||
"MODEDGES": cg_modedges,
|
||||
"NODES": cg_nodes,
|
||||
"EDGES": cg_edges,
|
||||
"CHAINS": chains,
|
||||
"STAT": {"nodes": N, "edges": len(cg_edges), "callees": len(set(b for _, b in edges)),
|
||||
"modxedge": len(modedge), "maxdepth": max((len(c) for c in chains), default=0)},
|
||||
}
|
||||
payload = json.dumps(data, ensure_ascii=False, separators=(",", ":"))
|
||||
with open(OUT, "w", encoding="utf-8") as w:
|
||||
w.write("/* 자동생성: tools/gen_callgraph.py — svc tpcall 호출그래프 (레이아웃 사전계산). 손대지 말 것. */\n")
|
||||
w.write("window.CALLGRAPH = " + payload + ";\n")
|
||||
|
||||
# index.html 에 인라인 (게이트웨이는 index.html 만 서빙, 외부 .js 로드 불가).
|
||||
# gen_screens.py 와 동일하게 마커 블록을 교체한다(재실행 idempotent).
|
||||
import re as _re
|
||||
IDX = os.path.join(os.path.dirname(__file__), "..", "app", "ui", "index.html")
|
||||
h = open(IDX, encoding="utf-8").read()
|
||||
block = ("<script>/* CG:START 자동생성 호출그래프 데이터 (tools/gen_callgraph.py) */\n"
|
||||
"window.CALLGRAPH=" + payload + ";\n/* CG:END */</script>")
|
||||
if "/* CG:START" in h:
|
||||
h = _re.sub(r'<script>/\* CG:START.*?/\* CG:END \*/</script>', lambda m: block, h, count=1, flags=_re.S)
|
||||
else:
|
||||
# 매니페스트 스크립트 바로 앞에 삽입 → 메인 앱보다 먼저 로드됨
|
||||
h = h.replace('<script>\n/* === 화면 매니페스트', block + '\n<script>\n/* === 화면 매니페스트', 1)
|
||||
open(IDX, "w", encoding="utf-8").write(h)
|
||||
print(f"callgraph.js: nodes={N} edges={len(cg_edges)} modxedge={len(modedge)} "
|
||||
f"maxdepth={data['STAT']['maxdepth']} chains={len(chains)}")
|
||||
for c in chains:
|
||||
print(" 체인:", " → ".join(c))
|
||||
Loading…
Add table
Add a link
Reference in a new issue