IR 자료: 마이그레이션 난이도 브리핑 (3축 전량 실측)

"Forge가 얼마나 복잡한 레거시를 다룰 수 있는가"를 증명하는 Before 자산.
지표는 전부 소스에서 자동 집계(추정·과장 없음).

- tools/gen_metrics.py: 소스 실측 → app/ui/metrics.js + index.html 인라인
  · 축A 규모: 파일 4,626 / 33만 LOC / EXEC SQL 23,742 / tpcall 989 /
    tpbegin 1,234 / UBF 46,904 / 테이블 75 / 카피북 2,207
  · 축B 자동변환 실패지점: ATMI·XA2PC(6단)·ECPG·UBF·카피북팬아웃·매크로
    각각 실측 카운트 + Java/Spring 이전이 어려운 이유
  · 축B 거래추적: ACQUIRE 6프로그램·6XA브랜치, PAY_APPROVE 5·5, CL_DAILY 6·6
    (프로그램→테이블쓰기→XA브랜치 트리)
  · 축C 레거시 인벤토리: acq_common.h 139프로그램 공유, 매크로 2,247,
    sqlca 수동분기 18,719, EXEC밀도 9.8/파일
- 포털 신규 화면 "마이그레이션 난이도"(AR0002, #mig): 위 3축을 한 화면에
- docs/MIGRATION_COMPLEXITY.md: 피치덱용 브리프 (실동작 증거 + 발견된 실결함 4건)
- README: IR 자료 안내 추가

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
hyeongwoo 2026-07-22 13:18:40 +09:00
parent 5b572989e3
commit 46ff41b633
5 changed files with 352 additions and 1 deletions

View file

@ -8,6 +8,12 @@
> 이 C/ATMI 시스템(2,000+본)을 Java Spring Boot로 전환하는 시나리오를 검증한다. > 이 C/ATMI 시스템(2,000+본)을 Java Spring Boot로 전환하는 시나리오를 검증한다.
> 컴파일만 되는 스텁이 아니라 **상용급 TP 미들웨어에서 실제로 도는** 시스템이라는 점이 핵심이다. > 컴파일만 되는 스텁이 아니라 **상용급 TP 미들웨어에서 실제로 도는** 시스템이라는 점이 핵심이다.
> **IR·데모용 자료** — [`docs/MIGRATION_COMPLEXITY.md`](docs/MIGRATION_COMPLEXITY.md):
> "Forge가 얼마나 복잡한 레거시를 다룰 수 있는가"를 3축(양적 규모·자동변환 실패지점·
> 실전 레거시성)으로 **전량 실측**해 정리. 포털에서도 실시간 확인:
> `#arch`(호출그래프 552노드·41 모듈간 경로), `#mig`(마이그레이션 난이도 브리핑).
> 지표는 전부 소스에서 자동 집계된다(`tools/gen_callgraph.py`, `tools/gen_metrics.py`).
--- ---
## 1. 스택 ## 1. 스택

File diff suppressed because one or more lines are too long

2
app/ui/metrics.js Normal file
View file

@ -0,0 +1,2 @@
/* 자동생성: tools/gen_metrics.py — 소스 실측 마이그레이션 난이도 지표. 손대지 말 것. */
window.METRICS={"SCALE":[{"k":"소스 파일 (.pgc/.c/.h)","v":4626,"u":"개"},{"k":"총 코드 라인","v":330642,"u":"LOC"},{"k":"온라인 서비스","v":1463,"u":"본"},{"k":"배치 잡","v":506,"u":"본"},{"k":"임베디드 SQL(EXEC SQL)","v":23742,"u":"블록"},{"k":"Tuxedo tpcall 호출","v":989,"u":"곳"},{"k":"전역 트랜잭션 tpbegin","v":1234,"u":"곳"},{"k":"UBF 버퍼 필드접근","v":46904,"u":"곳"},{"k":"DB 테이블","v":75,"u":"개"},{"k":"카피북 헤더(.h)","v":2207,"u":"개"}],"HAZARDS":[{"t":"Tuxedo ATMI (tpcall/tpservice/tpreturn)","n":989,"u":"호출","why":"Java·Spring에 1:1 대응 API가 없다. 서비스 호출 모델 자체를 REST/메시지로 재설계해야 하며, 문법 치환으로는 불가능."},{"t":"XA 2PC 전역 트랜잭션 (최대 6단 중첩)","n":1234,"u":"tpbegin","why":"형제 XA 브랜치 격리·2단계 커밋 의미론이 Spring @Transactional 경계와 다르다. 잘못 옮기면 교착·이중전기가 그대로 재현된다."},{"t":"ECPG 임베디드 SQL (호스트변수·커서·sqlca)","n":23742,"u":"블록","why":"EXEC SQL 블록마다 호스트변수 바인딩·sqlcode 분기·커서 수명을 JPA/MyBatis 의미로 옮겨야 한다. 자동 변환기가 가장 많이 깨지는 지점."},{"t":"UBF(FML32) 타입리스 버퍼","n":46904,"u":"필드접근","why":"필드ID 기반 동적 버퍼라 타입·필수여부가 컴파일타임에 없다. Java DTO로 옮기려면 47,000여 접근점의 계약을 사람이 복원해야 한다."},{"t":"카피북 공유(#include) 팬아웃","n":6892,"u":"참조","why":"한 카피북을 수십 프로그램이 공유해 구조체 변경이 전방위로 파급된다. 경계 없는 결합이라 모듈 단위 절단이 어렵다."},{"t":"전처리 매크로(#define)","n":2247,"u":"정의","why":"매크로가 코드 생성·분기를 숨겨 정적 분석을 방해한다. 전개 후에야 실제 로직이 드러난다."}],"TRACE":[{"root":"ACQUIRE","tree":{"svc":"ACQUIRE","mod":"ac","wr":["cm_log"],"children":[{"svc":"RECONCILE","mod":"rc","wr":["cm_log","ledger"],"children":[{"svc":"SETTLE","mod":"st","wr":["ledger","settlement"],"children":[{"svc":"ST_MDR","mod":"st","wr":["ledger"],"children":[{"svc":"LG_SETTLEPOST","mod":"lg","wr":["cm_log"],"children":[{"svc":"LG_BALCHK","mod":"lg","wr":["cm_log"],"children":[]}]}]}]}]}]},"stat":{"programs":6,"tables":3,"xa_branches":6,"tablelist":["cm_log","ledger","settlement"]}},{"root":"PAY_APPROVE","tree":{"svc":"PAY_APPROVE","mod":"py","wr":["cm_log","py_payout"],"children":[{"svc":"PAY_FILEGEN","mod":"py","wr":["cm_log","py_file","py_payout"],"children":[{"svc":"PAY_ACCTCHK","mod":"py","wr":["cm_log"],"children":[{"svc":"LG_POST","mod":"lg","wr":["cm_log"],"children":[{"svc":"LG_BALCHK","mod":"lg","wr":["cm_log"],"children":[]}]}]}]}]},"stat":{"programs":5,"tables":3,"xa_branches":5,"tablelist":["cm_log","py_file","py_payout"]}},{"root":"CL_DAILY","tree":{"svc":"CL_DAILY","mod":"cl","wr":["cm_log"],"children":[{"svc":"VL_SALES","mod":"vl","wr":[],"children":[{"svc":"RC_EXCEPTION","mod":"rc","wr":["cm_log"],"children":[{"svc":"CM_LOG","mod":"cm","wr":[],"children":[]}]}]},{"svc":"LG_CLOSE_LINK","mod":"lg","wr":["cm_log","lg_ledger_line"],"children":[{"svc":"CL_LOCK","mod":"cl","wr":["SET","cl_close_log","cl_period_lock","cm_log"],"children":[]}]}]},"stat":{"programs":6,"tables":5,"xa_branches":6,"tablelist":["SET","cl_close_log","cl_period_lock","cm_log","lg_ledger_line"]}}],"LEGACY":{"copybook_total":2207,"copybook_shared_max":139,"copybook_shared_name":"acq_common.h","macro_defs":2247,"goto":64,"userlog":17122,"sqlca_checks":18719,"embedded_sql_density":9.8}};

View file

@ -0,0 +1,102 @@
# acquire-core-x — 마이그레이션 난이도 브리핑 (IR)
> **한 줄 요약.** 이 저장소는 "Klaro Forge가 어느 정도로 복잡하게 얽힌 레거시까지
> 다룰 수 있는가"를 증명하기 위한 **실동작 예제(Before)** 다. 실제 카드 매입시스템
> (Tuxedo / Pro\*C / XA / Oracle 계열)의 아키텍처를 그대로 재현하되, 오픈소스 스택
> (Enduro/X · ECPG · PostgreSQL)으로 **실제로 빌드·부팅·거래가 되게** 만들었다.
> 아래 수치는 전부 소스코드에서 직접 집계한 값이며 과장이 없다.
> 포털에서 실시간으로 같은 수치를 볼 수 있다: `#mig`(마이그레이션 난이도), `#arch`(호출그래프).
---
## 왜 이게 "어려운 마이그레이션"인가 — 3축
### 축 A. 양적 압도 (규모)
| 지표 | 실측 |
|---|---|
| 소스 파일 (.pgc/.c/.h) | **4,626 개** |
| 총 코드 라인 | **330,642 LOC** |
| 온라인 서비스 | **1,463 본** |
| 배치 잡 | **506 본** |
| 임베디드 SQL (EXEC SQL) 블록 | **23,742** |
| Tuxedo tpcall 호출부 | **989** |
| 전역 트랜잭션 tpbegin | **1,234** |
| UBF 버퍼 필드접근 | **46,904** |
| DB 테이블 | **75** |
| 카피북 헤더(.h) | **2,207** |
BAIS(비씨카드 글로벌 매입시스템) RFI 기준 온라인 1,466 · 배치 503과 **1:1 규모**로 맞췄다.
### 축 B. 질적 난이도 (자동 변환이 실패하는 지점)
단순 문법 치환기·규칙 기반 변환기가 통째로 깨지는 지점들이다. 이게 곧 Forge의 존재 이유다.
| 레거시 요소 | 실측 | Java·Spring 이전이 어려운 이유 |
|---|---|---|
| Tuxedo ATMI (tpcall/tpservice/tpreturn) | 989 호출 | Java·Spring에 1:1 대응 API가 없다. 서비스 호출 모델 자체를 REST/메시지로 **재설계**해야 한다. |
| XA 2PC 전역 트랜잭션 (최대 **6단 중첩**) | 1,234 tpbegin | 형제 XA 브랜치 격리·2단계 커밋 의미론이 Spring `@Transactional` 경계와 다르다. 잘못 옮기면 **교착·이중전기**가 그대로 재현된다. |
| ECPG 임베디드 SQL (호스트변수·커서·sqlca) | 23,742 블록 | 블록마다 호스트변수 바인딩·`sqlcode` 분기·커서 수명을 JPA/MyBatis 의미로 옮겨야 한다. 자동 변환기가 가장 많이 깨지는 지점. |
| UBF(FML32) 타입리스 버퍼 | 46,904 접근 | 필드ID 기반 동적 버퍼라 타입·필수여부가 컴파일타임에 없다. Java DTO로 옮기려면 4.6만 접근점의 계약을 **사람이 복원**해야 한다. |
| 카피북 공유(#include) 팬아웃 | 6,892 참조 | 한 카피북을 수십 프로그램이 공유해 구조체 변경이 전방위로 파급된다. 모듈 단위 절단이 어렵다. |
| 전처리 매크로(#define) | 2,247 정의 | 매크로가 코드 생성·분기를 숨겨 정적 분석을 방해한다. 전개 후에야 실제 로직이 드러난다. |
**거래 하나의 파급** — "매입 1건이 몇 개 프로그램·테이블·트랜잭션 경계를 건드리나":
| 대표 거래 | 연쇄 프로그램 | XA 브랜치(2PC) | 기록 테이블 | 체인 |
|---|---|---|---|---|
| `ACQUIRE` (매입 접수) | 6 | 6 | 3 | ACQUIRE→RECONCILE→SETTLE→ST_MDR→LG_SETTLEPOST→LG_BALCHK |
| `PAY_APPROVE` (지급 승인) | 5 | 5 | 3 | PAY_APPROVE→PAY_FILEGEN→PAY_ACCTCHK→LG_POST→LG_BALCHK |
| `CL_DAILY` (일마감) | 6 | 6 | 5 | CL_DAILY→VL_SALES→RC_EXCEPTION→CM_LOG / →LG_CLOSE_LINK→CL_LOCK |
하나의 `tpbegin` 아래 여러 XA 브랜치가 서로 다른 테이블을 잠근 채 2단계 커밋으로 원자성을
보장한다. 이 경계를 잘못 옮기면 형제 브랜치 교착이 재현된다 — **실제로 이 저장소를 만들며
그 교착을 밟고 고쳤다**(아래 "발견된 실결함" 참조).
### 축 C. 리얼리티 (실전 레거시임)
"깔끔하게 만든 예제"가 아니라 실전 레거시 특유의 냄새가 실측된다.
| 레거시 특성 | 실측 |
|---|---|
| 가장 많이 공유되는 카피북 | `acq_common.h`**139개** 프로그램이 #include (구조체 1줄 바꾸면 139곳 파급) |
| 전처리 매크로(#define) | 2,247 개 |
| `sqlca.sqlcode` 수동 에러분기 | **18,719 곳** (예외 대신 리턴코드 분기 — 전부 개별 이관 필요) |
| 임베디드 SQL 밀도 | 파일당 평균 **9.8 블록** |
| 운영 `userlog` 호출 | 17,122 곳 |
| `goto` 잔존 | 64 곳 |
---
## 실동작 증거 (예제가 "그림"이 아님)
- **빌드**: `docker compose up` → Enduro/X 소스빌드 + ECPG 프리컴파일 + 11 모듈서버 + 506 배치, 오류 0.
- **부팅**: 45 프로세스, 1,466 서비스 AVAIL.
- **실거래**: 6단 XA 체인 `ACQUIRE→…→LG_BALCHK` 실행 → `COMMIT OK`, 미결 2PC 0.
- **전수 스모크**: 체인 보유 477 서비스 100% 응답, 교착/타임아웃 0, 최대 530ms.
- **업무 포털**: 운영 대시보드·매입·정산·가맹점·원장·아키텍처·난이도 화면. ~17.5만 행의 운영 데이터.
## 발견된 실결함 (레거시가 실제로 위험하다는 증거)
예제를 만들며 실행 중에 밟은, **정적 분석으로는 잡히지 않는** 결함들 — 이게 곧 마이그레이션 리스크의 실물이다.
1. **크로스브랜치 핫로우 교착** — 한 글로벌 트랜잭션의 형제 XA 브랜치가 같은 행을 UPDATE → 60초 블록 후 트랜잭션 타임아웃. (`mg_channel`, `lg_merch_bal` 등)
2. **원장 이중전기** — 같은 정산건이 다이아몬드 호출로 원장에 두 번 기표되던 회계 오류.
3. **PK 채번 충돌**`max(log_id)+1``nextval` 혼용으로 형제 브랜치가 동일 PK INSERT → 인덱스 블록.
4. **스택 파괴**`strncpy(out, val, 127)` 의 NUL 패딩이 호출자 스택 112바이트를 훼손 → 세그폴트. (DB 락이 0인데 60초 블록되는 증상)
모두 실행 검증에서만 드러났고, 소스에서 고쳐 재검증했다. **"자동 문법 변환으로 옮기면 이 결함들이 신규 시스템에 그대로 이식된다"** 가 핵심 메시지다.
---
## 재현 방법
```bash
docker compose -f docker/docker-compose.yml up -d --build # 빌드+부팅
# 포털: http://localhost:8090 (#arch 호출그래프 · #mig 마이그레이션 난이도)
python3 tools/gen_callgraph.py # 호출그래프 재추출 (소스 기준)
python3 tools/gen_metrics.py # 난이도 지표 재집계 (소스 기준)
```
지표는 전부 소스에서 자동 집계된다(`tools/gen_metrics.py`, `tools/gen_callgraph.py`).
"이 숫자 진짜냐"는 질문에 코드로 답할 수 있다.

167
tools/gen_metrics.py Normal file
View file

@ -0,0 +1,167 @@
#!/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 = ("<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"]))