#!/usr/bin/env python3 """ gen_metrics.py — IR·데모용 "마이그레이션 난이도" 실측 지표를 뽑아 app/ui/metrics.js (+ index.html 인라인) 로 굽는다. 전부 소스에서 계산(지어내지 않음). 산출: SCALE : 양적 규모 (파일/LOC/EXEC SQL/tpcall/tpbegin/UBF/테이블/배치) ← 축A HAZARDS : 자동변환 실패 지점 (설명 + 실측 카운트 + 왜 어려운지) ← 축B TRACE : 대표 거래(ACQUIRE 등)가 건드리는 프로그램/테이블/XA브랜치 트리 ← 축B LEGACY : 레거시 실측 인벤토리 (카피북 팬아웃/매크로/임베디드SQL밀도 등) ← 축C """ import glob, re, os, json, collections ROOT = os.path.join(os.path.dirname(__file__), "..") SRC = os.path.join(ROOT, "app", "src") SCHEMA = os.path.join(ROOT, "db", "schema.d") OUT_JS = os.path.join(ROOT, "app", "ui", "metrics.js") IDX = os.path.join(ROOT, "app", "ui", "index.html") def sh_count(pat_files, needle_re): n = 0 rx = re.compile(needle_re) for f in pat_files: n += len(rx.findall(open(f, encoding="utf-8", errors="ignore").read())) return n pgc = glob.glob(os.path.join(SRC, "*/**/*.pgc"), recursive=True) c_files = glob.glob(os.path.join(SRC, "**/*.c"), recursive=True) h_files = glob.glob(os.path.join(SRC, "**/*.h"), recursive=True) svc_files = glob.glob(os.path.join(SRC, "*/svc/*.pgc")) batch_files = glob.glob(os.path.join(SRC, "*/batch/*.pgc")) dbio_files = glob.glob(os.path.join(SRC, "*/dbio/*.pgc")) def loc(files): return sum(sum(1 for _ in open(f, encoding="utf-8", errors="ignore")) for f in files) # ── 축 A: 규모 ────────────────────────────────────────────────────────────── tables = sh_count(glob.glob(os.path.join(SCHEMA, "*.sql")), r"(?i)create table") SCALE = [ {"k": "소스 파일 (.pgc/.c/.h)", "v": len(pgc) + len(c_files) + len(h_files), "u": "개"}, {"k": "총 코드 라인", "v": loc(pgc + c_files + h_files), "u": "LOC"}, {"k": "온라인 서비스", "v": len(svc_files), "u": "본"}, {"k": "배치 잡", "v": len(batch_files), "u": "본"}, {"k": "임베디드 SQL(EXEC SQL)", "v": sh_count(pgc, r"EXEC SQL"), "u": "블록"}, {"k": "Tuxedo tpcall 호출", "v": sh_count(pgc, r"\btpcall\s*\("), "u": "곳"}, {"k": "전역 트랜잭션 tpbegin", "v": sh_count(pgc, r"\btpbegin\b"), "u": "곳"}, {"k": "UBF 버퍼 필드접근", "v": sh_count(pgc, r"\b(?:C?Bget|C?Bchg|getl|setl|gets_)\s*\("), "u": "곳"}, {"k": "DB 테이블", "v": tables, "u": "개"}, {"k": "카피북 헤더(.h)", "v": len(h_files), "u": "개"}, ] # ── 축 B: 자동변환 실패 지점 ───────────────────────────────────────────────── xa_deep = sh_count(pgc, r"\btpbegin\b") HAZARDS = [ {"t": "Tuxedo ATMI (tpcall/tpservice/tpreturn)", "n": sh_count(pgc, r"\btpcall\s*\("), "u": "호출", "why": "Java·Spring에 1:1 대응 API가 없다. 서비스 호출 모델 자체를 REST/메시지로 재설계해야 하며, 문법 치환으로는 불가능."}, {"t": "XA 2PC 전역 트랜잭션 (최대 6단 중첩)", "n": xa_deep, "u": "tpbegin", "why": "형제 XA 브랜치 격리·2단계 커밋 의미론이 Spring @Transactional 경계와 다르다. 잘못 옮기면 교착·이중전기가 그대로 재현된다."}, {"t": "ECPG 임베디드 SQL (호스트변수·커서·sqlca)", "n": sh_count(pgc, r"EXEC SQL"), "u": "블록", "why": "EXEC SQL 블록마다 호스트변수 바인딩·sqlcode 분기·커서 수명을 JPA/MyBatis 의미로 옮겨야 한다. 자동 변환기가 가장 많이 깨지는 지점."}, {"t": "UBF(FML32) 타입리스 버퍼", "n": sh_count(pgc, r"\b(?:C?Bget|C?Bchg|getl|setl|gets_)\s*\("), "u": "필드접근", "why": "필드ID 기반 동적 버퍼라 타입·필수여부가 컴파일타임에 없다. Java DTO로 옮기려면 47,000여 접근점의 계약을 사람이 복원해야 한다."}, {"t": "카피북 공유(#include) 팬아웃", "n": sh_count(pgc, r"#include"), "u": "참조", "why": "한 카피북을 수십 프로그램이 공유해 구조체 변경이 전방위로 파급된다. 경계 없는 결합이라 모듈 단위 절단이 어렵다."}, {"t": "전처리 매크로(#define)", "n": sh_count(h_files + pgc, r"#define"), "u": "정의", "why": "매크로가 코드 생성·분기를 숨겨 정적 분석을 방해한다. 전개 후에야 실제 로직이 드러난다."}, ] # ── 축 B: 거래 추적 (프로그램→테이블→XA브랜치) ──────────────────────────────── # 각 svc 가 건드리는 테이블 (svc 파일 + 같은 모듈 dbio 전체를 근사) def tables_of(path): txt = open(path, encoding="utf-8", errors="ignore").read() w = set(re.findall(r"(?i)(?:INSERT\s+INTO|UPDATE|DELETE\s+FROM)\s+([a-z_][a-z0-9_]*)", txt)) r = set(re.findall(r"(?i)FROM\s+([a-z_][a-z0-9_]*)", txt)) return w, r svc_tbl = {} svc_path = {} for f in svc_files: name = os.path.basename(f)[:-4] svc_path[name] = f svc_tbl[name] = tables_of(f) # 호출 그래프 adj = collections.defaultdict(list) for f in svc_files: caller = os.path.basename(f)[:-4] for callee in re.findall(r'tpcall\("([A-Z_0-9]+)"', open(f, encoding="utf-8", errors="ignore").read()): if callee != caller and callee not in adj[caller]: adj[caller].append(callee) def trace(root, depth=0, seen=None): seen = seen or set() w, r = svc_tbl.get(root, (set(), set())) node = {"svc": root, "mod": os.path.normpath(svc_path[root]).split(os.sep)[-3] if root in svc_path else "?", "wr": sorted(w), "children": []} if root in seen or depth >= 6: return node seen = seen | {root} for callee in adj.get(root, []): if callee in svc_path: node["children"].append(trace(callee, depth + 1, seen)) return node def flat_stats(tree): progs, wt, branches = set(), set(), 0 def walk(n): nonlocal branches progs.add(n["svc"]); branches += 1 for t in n["wr"]: wt.add(t) for c in n["children"]: walk(c) walk(tree) return {"programs": len(progs), "tables": len(wt), "xa_branches": branches, "tablelist": sorted(wt)} TRACE = [] for root in ["ACQUIRE", "PAY_APPROVE", "CL_DAILY"]: if root in svc_path: tr = trace(root) st = flat_stats(tr) TRACE.append({"root": root, "tree": tr, "stat": st}) # ── 축 C: 레거시 실측 인벤토리 ─────────────────────────────────────────────── inc = collections.Counter() for f in pgc: for h in re.findall(r'#include\s+"([^"]+)"', open(f, encoding="utf-8", errors="ignore").read()): inc[h] += 1 top_copybook = inc.most_common(1) LEGACY = { "copybook_total": len(h_files), "copybook_shared_max": (top_copybook[0][1] if top_copybook else 0), "copybook_shared_name": (top_copybook[0][0] if top_copybook else ""), "macro_defs": sh_count(h_files + pgc, r"#define"), "goto": sh_count(pgc, r"\bgoto\b"), "userlog": sh_count(pgc, r"\buserlog\s*\("), "sqlca_checks": sh_count(pgc, r"sqlca\.sqlcode"), "embedded_sql_density": round(sh_count(pgc, r"EXEC SQL") / max(1, len(pgc)), 1), } data = {"SCALE": SCALE, "HAZARDS": HAZARDS, "TRACE": TRACE, "LEGACY": LEGACY} payload = json.dumps(data, ensure_ascii=False, separators=(",", ":")) with open(OUT_JS, "w", encoding="utf-8") as w: w.write("/* 자동생성: tools/gen_metrics.py — 소스 실측 마이그레이션 난이도 지표. 손대지 말 것. */\n") w.write("window.METRICS=" + payload + ";\n") # index.html 인라인 (마커 idempotent) h = open(IDX, encoding="utf-8").read() block = ("") if "/* MET:START" in h: h = re.sub(r'', lambda m: block, h, count=1, flags=re.S) else: h = h.replace('