acquire-core-x/tools/gen_metrics.py
hyeongwoo e1729d6edc Wave H: Oracle 계보(As-Is) 심기 + 시스템 조망 재편(운영 콘솔)
■ Oracle 계보 — "원본은 Oracle 19c + Pro*C 였다"
현행 코드(ECPG/PostgreSQL)가 Oracle 시스템의 1차 이관본임을 콘크리트하게 보존.
- legacy-oracle/proc/*.pc (7본): ACQUIRE/AUTH_APPROVE/ST_MDR/SETTLE/RECONCILE/
  CL_DAILY/PAY_FILEGEN. 순수 Oracle Pro*C 방언 — sqlca/oraca, VARCHAR,
  WHENEVER SQLERROR GOTO, NVL/DECODE/SYSDATE/DUAL/ROWNUM/(+)/CONNECT BY/
  .NEXTVAL/TO_DATE, PL/SQL 패키지 호출, COMMIT WORK. 컴파일 대상 아님(As-Is 원본).
- legacy-oracle/plsql/*.sql (2본): PKG_SETTLE(정산), PKG_LEDGER(복식부기 기표).
  VARCHAR2/%TYPE/%ROWTYPE/CURSOR/EXCEPTION/RAISE_APPLICATION_ERROR.
- docs/ORACLE_PROVENANCE.md: As-Is(Oracle)→현행(ECPG)→To-Be(Spring Boot) 3단 계보
  + Oracle→PostgreSQL 방언 이관 매핑 15종 + "왜 DB는 PostgreSQL인가".
- gen_metrics.py: legacy-oracle 실측(원본 9파일 1,827LOC, 방언 12종 히트) →
  마이그레이션 난이도 화면에 "④ Oracle 계보" 패널 추가.

■ 시스템 조망 재편 — 실제 매입시스템 운영 콘솔
"마이그레이션 난이도"(Forge 관점 메타)를 시스템 조망에서 분리하고, 시스템 자체의
상태·아키텍처를 보여주는 운영 화면 신설.
- 신규 "시스템 현황"(#sys): 헬스 KPI(서버11/서비스1466/미결2PC/당일매입/채널UP/
  큐적체/오류) + 아키텍처 토폴로지(채널·카드망→전문GW→승인·매입→정산·지급→
  원장·마감·대사·검증→마스터·공통 계층도) + 채널상태 + 큐적체 + 처리량·오류율
  + 정산사이클(마감·순액정산·클리어링·프레젠트먼트). 전부 실측 조회.
- nav 재편: [시스템 조망]=시스템현황·아키텍처토폴로지·정산경제,
  [전환 분석(Forge)]=마이그레이션난이도.
- 토폴로지 모듈박스 렌더 버그(return ASI) 수정.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 12:10:37 +09:00

197 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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),
}
# ── Oracle 계보 (As-Is 원본 legacy-oracle/ 실측) ─────────────────────────────
ora_proc = glob.glob(os.path.join(ROOT, "legacy-oracle", "proc", "*.pc"))
ora_plsql = glob.glob(os.path.join(ROOT, "legacy-oracle", "plsql", "*.sql"))
ora_files = ora_proc + ora_plsql
def ora_hits(pat): return sh_count(ora_files, pat) if ora_files else 0
ORACLE = {
"asis_files": len(ora_files),
"asis_proc": len(ora_proc),
"asis_plsql": len(ora_plsql),
"asis_loc": loc(ora_files) if ora_files else 0,
# As-Is 원본에서 실측한 Oracle 방언 (현행 이관에서 옮긴 것들)
"dialects": [
{"t": "PL/SQL 패키지 호출 (pkg_x.proc)", "n": ora_hits(r"\bpkg_[a-z_]+\.")},
{"t": "NVL / NVL2 널가드", "n": ora_hits(r"\bNVL2?\s*\(")},
{"t": "DECODE 다분기", "n": ora_hits(r"\bDECODE\s*\(")},
{"t": "SYSDATE / SYSTIMESTAMP", "n": ora_hits(r"\bSYS(DATE|TIMESTAMP)\b")},
{"t": "FROM DUAL", "n": ora_hits(r"\bFROM\s+DUAL\b")},
{"t": "시퀀스 .NEXTVAL", "n": ora_hits(r"\.NEXTVAL\b")},
{"t": "ROWNUM 의사컬럼", "n": ora_hits(r"\bROWNUM\b")},
{"t": "구식 외부조인 (+)", "n": ora_hits(r"\(\+\)")},
{"t": "CONNECT BY 계층질의", "n": ora_hits(r"\bCONNECT\s+BY\b")},
{"t": "WHENEVER SQLERROR 라벨분기", "n": ora_hits(r"WHENEVER\s+SQLERROR")},
{"t": "TO_DATE / TO_CHAR 포맷", "n": ora_hits(r"\bTO_(DATE|CHAR)\s*\(")},
{"t": "RAISE_APPLICATION_ERROR", "n": ora_hits(r"RAISE_APPLICATION_ERROR")},
],
}
data = {"SCALE": SCALE, "HAZARDS": HAZARDS, "TRACE": TRACE, "LEGACY": LEGACY, "ORACLE": ORACLE}
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 = ("<script>/* MET:START 자동생성 마이그레이션 지표 (tools/gen_metrics.py) */\n"
"window.METRICS=" + payload + ";\n/* MET:END */</script>")
if "/* MET:START" in h:
h = re.sub(r'<script>/\* MET:START.*?/\* MET:END \*/</script>', lambda m: block, h, count=1, flags=re.S)
else:
h = h.replace('<script>/* CG:START', block + '\n<script>/* CG:START', 1)
open(IDX, "w", encoding="utf-8").write(h)
print("metrics: scale=%d hazards=%d trace=%d" % (len(SCALE), len(HAZARDS), len(TRACE)))
for t in TRACE:
s = t["stat"]
print(f" 거래 {t['root']}: 프로그램 {s['programs']} · 테이블 {s['tables']} · XA브랜치 {s['xa_branches']}")
print(" 레거시: 카피북 %d(최대공유 %d×%s) 매크로 %d sqlca분기 %d EXEC밀도 %.1f/파일"
% (LEGACY["copybook_total"], LEGACY["copybook_shared_max"], LEGACY["copybook_shared_name"],
LEGACY["macro_defs"], LEGACY["sqlca_checks"], LEGACY["embedded_sql_density"]))
print(" Oracle As-Is: 원본 %d파일(proc %d/plsql %d) %dLOC · 방언히트 %d"
% (ORACLE["asis_files"], ORACLE["asis_proc"], ORACLE["asis_plsql"], ORACLE["asis_loc"],
sum(1 for d in ORACLE["dialects"] if d["n"] > 0)))