■ 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>
197 lines
11 KiB
Python
197 lines
11 KiB
Python
#!/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)))
|